浏览代码

Merge pull request #935 from 0xJacky/feat/backup

feat: backup and restore
Jacky 1 月之前
父节点
当前提交
b1a7866725
共有 100 个文件被更改,包括 10160 次插入1745 次删除
  1. 0 24
      api/api.go
  2. 6 6
      api/certificate/acme_user.go
  3. 11 11
      api/certificate/certificate.go
  4. 6 6
      api/certificate/dns_credential.go
  5. 2 2
      api/cluster/environment.go
  6. 2 2
      api/cluster/node.go
  7. 10 10
      api/config/add.go
  8. 9 8
      api/config/get.go
  9. 6 5
      api/config/list.go
  10. 4 4
      api/config/mkdir.go
  11. 11 11
      api/config/modify.go
  12. 10 10
      api/config/rename.go
  13. 2 2
      api/crypto/crypto.go
  14. 5 5
      api/nginx/nginx.go
  15. 5 5
      api/notification/notification.go
  16. 4 4
      api/openai/store.go
  17. 3 3
      api/settings/settings.go
  18. 4 4
      api/sites/advance.go
  19. 6 6
      api/sites/auto_cert.go
  20. 3 3
      api/sites/duplicate.go
  21. 9 8
      api/sites/list.go
  22. 12 12
      api/sites/site.go
  23. 2 3
      api/streams/advance.go
  24. 3 3
      api/streams/duplicate.go
  25. 11 12
      api/streams/streams.go
  26. 42 0
      api/system/backup.go
  27. 390 0
      api/system/backup_test.go
  28. 35 5
      api/system/install.go
  29. 135 0
      api/system/restore.go
  30. 17 0
      api/system/router.go
  31. 3 3
      api/system/upgrade.go
  32. 8 6
      api/template/template.go
  33. 8 8
      api/user/2fa.go
  34. 7 7
      api/user/auth.go
  35. 10 10
      api/user/casdoor.go
  36. 5 5
      api/user/otp.go
  37. 10 10
      api/user/passkey.go
  38. 3 2
      api/user/recovery.go
  39. 2 0
      app/components.d.ts
  40. 57 0
      app/src/api/backup.ts
  41. 6 1
      app/src/api/install.ts
  42. 355 0
      app/src/components/SystemRestore/SystemRestoreContent.vue
  43. 60 0
      app/src/constants/errors/backup.ts
  44. 4 0
      app/src/constants/errors/system.ts
  45. 474 52
      app/src/language/ar/app.po
  46. 468 52
      app/src/language/de_DE/app.po
  47. 471 52
      app/src/language/en/app.po
  48. 468 52
      app/src/language/es/app.po
  49. 472 51
      app/src/language/fr_FR/app.po
  50. 464 53
      app/src/language/ko_KR/app.po
  51. 433 58
      app/src/language/messages.pot
  52. 444 97
      app/src/language/ru_RU/app.po
  53. 467 52
      app/src/language/tr_TR/app.po
  54. 464 53
      app/src/language/vi_VN/app.po
  55. 445 57
      app/src/language/zh_CN/app.po
  56. 475 52
      app/src/language/zh_TW/app.po
  57. 38 0
      app/src/lib/http/client.ts
  58. 78 0
      app/src/lib/http/error.ts
  59. 14 184
      app/src/lib/http/index.ts
  60. 160 0
      app/src/lib/http/interceptors.ts
  61. 25 0
      app/src/lib/http/types.ts
  62. 1 0
      app/src/lib/nprogress/nprogress.ts
  63. 37 310
      app/src/routes/index.ts
  64. 16 0
      app/src/routes/modules/auth.ts
  65. 61 0
      app/src/routes/modules/certificates.ts
  66. 35 0
      app/src/routes/modules/config.ts
  67. 14 0
      app/src/routes/modules/dashboard.ts
  68. 20 0
      app/src/routes/modules/environments.ts
  69. 10 0
      app/src/routes/modules/error.ts
  70. 36 0
      app/src/routes/modules/nginx_log.ts
  71. 14 0
      app/src/routes/modules/notifications.ts
  72. 14 0
      app/src/routes/modules/preference.ts
  73. 47 0
      app/src/routes/modules/sites.ts
  74. 24 0
      app/src/routes/modules/streams.ts
  75. 49 0
      app/src/routes/modules/system.ts
  76. 14 0
      app/src/routes/modules/terminal.ts
  77. 14 0
      app/src/routes/modules/user.ts
  78. 1 1
      app/src/version.json
  79. 119 59
      app/src/views/other/Install.vue
  80. 245 0
      app/src/views/system/Backup/BackupCreator.vue
  81. 18 0
      app/src/views/system/Backup/SystemRestore.vue
  82. 11 0
      app/src/views/system/Backup/index.vue
  83. 66 70
      go.mod
  84. 140 203
      go.sum
  85. 169 0
      internal/backup/backup.go
  86. 128 0
      internal/backup/backup_crypto.go
  87. 76 0
      internal/backup/backup_nginx_ui.go
  88. 466 0
      internal/backup/backup_test.go
  89. 290 0
      internal/backup/backup_zip.go
  90. 83 0
      internal/backup/errors.go
  91. 440 0
      internal/backup/restore.go
  92. 85 0
      internal/backup/utils.go
  93. 117 0
      internal/backup/version_test.go
  94. 82 0
      internal/middleware/encrypted_params.go
  95. 25 1
      internal/nginx/nginx.go
  96. 10 0
      internal/system/errors.go
  97. 8 4
      query/notifications.gen.go
  98. 5 1
      query/site_categories.gen.go
  99. 2 1
      router/routers.go
  100. 5 4
      settings/settings.go

+ 0 - 24
api/api.go

@@ -1,38 +1,14 @@
 package api
 
 import (
-	"errors"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/gin-gonic/gin"
-	"github.com/uozi-tech/cosy"
-	"github.com/uozi-tech/cosy/logger"
-	"gorm.io/gorm"
-	"net/http"
 )
 
 func CurrentUser(c *gin.Context) *model.User {
 	return c.MustGet("user").(*model.User)
 }
 
-func ErrHandler(c *gin.Context, err error) {
-	logger.GetLogger().Errorln(err)
-	var cErr *cosy.Error
-	switch {
-	case errors.Is(err, gorm.ErrRecordNotFound):
-		c.JSON(http.StatusNotFound, &cosy.Error{
-			Code:    http.StatusNotFound,
-			Message: gorm.ErrRecordNotFound.Error(),
-		})
-	case errors.As(err, &cErr):
-		c.JSON(http.StatusInternalServerError, cErr)
-	default:
-		c.JSON(http.StatusInternalServerError, &cosy.Error{
-			Code:    http.StatusInternalServerError,
-			Message: err.Error(),
-		})
-	}
-}
-
 func SetSSEHeaders(c *gin.Context) {
 	c.Header("Content-Type", "text/event-stream")
 	c.Header("Cache-Control", "no-cache")

+ 6 - 6
api/certificate/acme_user.go

@@ -1,14 +1,14 @@
 package certificate
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+
 	"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/spf13/cast"
 	"github.com/uozi-tech/cosy"
-	"net/http"
 )
 
 func GetAcmeUser(c *gin.Context) {
@@ -16,7 +16,7 @@ func GetAcmeUser(c *gin.Context) {
 	id := cast.ToUint64(c.Param("id"))
 	user, err := u.FirstByID(id)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	c.JSON(http.StatusOK, user)
@@ -83,17 +83,17 @@ func RegisterAcmeUser(c *gin.Context) {
 	u := query.AcmeUser
 	user, err := u.FirstByID(id)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	err = user.Register()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	_, err = u.Where(u.ID.Eq(id)).Updates(user)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	c.JSON(http.StatusOK, user)

+ 11 - 11
api/certificate/certificate.go

@@ -1,7 +1,9 @@
 package certificate
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+	"os"
+
 	"github.com/0xJacky/Nginx-UI/internal/cert"
 	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
@@ -12,8 +14,6 @@ import (
 	"github.com/go-acme/lego/v4/certcrypto"
 	"github.com/spf13/cast"
 	"github.com/uozi-tech/cosy"
-	"net/http"
-	"os"
 )
 
 type APICertificate struct {
@@ -74,7 +74,7 @@ func GetCert(c *gin.Context) {
 	certModel, err := q.FirstByID(cast.ToUint64(c.Param("id")))
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -114,7 +114,7 @@ func AddCert(c *gin.Context) {
 
 	err := certModel.Insert()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -127,7 +127,7 @@ func AddCert(c *gin.Context) {
 
 	err = content.WriteFile()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -153,7 +153,7 @@ func ModifyCert(c *gin.Context) {
 
 	certModel, err := q.FirstByID(id)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -169,7 +169,7 @@ func ModifyCert(c *gin.Context) {
 	})
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -182,7 +182,7 @@ func ModifyCert(c *gin.Context) {
 
 	err = content.WriteFile()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -218,7 +218,7 @@ func SyncCertificate(c *gin.Context) {
 
 	err := db.Where(certModel).FirstOrCreate(certModel).Error
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -231,7 +231,7 @@ func SyncCertificate(c *gin.Context) {
 
 	err = content.WriteFile()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 6 - 6
api/certificate/dns_credential.go

@@ -1,14 +1,14 @@
 package certificate
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/internal/cert/dns"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
 	"github.com/spf13/cast"
 	"github.com/uozi-tech/cosy"
-	"net/http"
 )
 
 func GetDnsCredential(c *gin.Context) {
@@ -18,7 +18,7 @@ func GetDnsCredential(c *gin.Context) {
 
 	dnsCredential, err := d.FirstByID(id)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	type apiDnsCredential struct {
@@ -62,7 +62,7 @@ func AddDnsCredential(c *gin.Context) {
 
 	err := d.Create(&dnsCredential)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -81,7 +81,7 @@ func EditDnsCredential(c *gin.Context) {
 
 	dnsCredential, err := d.FirstByID(id)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -93,7 +93,7 @@ func EditDnsCredential(c *gin.Context) {
 	})
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 2 - 2
api/cluster/environment.go

@@ -27,7 +27,7 @@ func GetEnvironment(c *gin.Context) {
 
 	env, err := envQuery.FirstByID(id)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -161,7 +161,7 @@ func DeleteEnvironment(c *gin.Context) {
 func LoadEnvironmentFromSettings(c *gin.Context) {
 	err := settings.ReloadCluster()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 2 - 2
api/cluster/node.go

@@ -3,7 +3,6 @@ package cluster
 import (
 	"net/http"
 
-	"github.com/0xJacky/Nginx-UI/api"
 	analytic2 "github.com/0xJacky/Nginx-UI/internal/analytic"
 	"github.com/0xJacky/Nginx-UI/internal/upgrader"
 	"github.com/0xJacky/Nginx-UI/internal/version"
@@ -11,6 +10,7 @@ import (
 	"github.com/gin-gonic/gin"
 	"github.com/shirou/gopsutil/v4/cpu"
 	"github.com/shirou/gopsutil/v4/disk"
+	"github.com/uozi-tech/cosy"
 )
 
 func GetCurrentNode(c *gin.Context) {
@@ -23,7 +23,7 @@ func GetCurrentNode(c *gin.Context) {
 
 	runtimeInfo, err := upgrader.GetRuntimeInfo()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	cpuInfo, _ := cpu.Info()

+ 10 - 10
api/config/add.go

@@ -1,7 +1,11 @@
 package config
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+	"os"
+	"path/filepath"
+	"time"
+
 	"github.com/0xJacky/Nginx-UI/internal/config"
 	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
@@ -10,10 +14,6 @@ import (
 	"github.com/gin-gonic/gin"
 	"github.com/sashabaranov/go-openai"
 	"github.com/uozi-tech/cosy"
-	"net/http"
-	"os"
-	"path/filepath"
-	"time"
 )
 
 func AddConfig(c *gin.Context) {
@@ -48,14 +48,14 @@ func AddConfig(c *gin.Context) {
 	if !helper.FileExists(dir) {
 		err := os.MkdirAll(dir, 0755)
 		if err != nil {
-			api.ErrHandler(c, err)
+			cosy.ErrHandler(c, err)
 			return
 		}
 	}
 
 	err := os.WriteFile(path, []byte(content), 0644)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -70,7 +70,7 @@ func AddConfig(c *gin.Context) {
 	q := query.Config
 	_, err = q.Where(q.Filepath.Eq(path)).Delete()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -83,13 +83,13 @@ func AddConfig(c *gin.Context) {
 
 	err = q.Create(cfg)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
 	err = config.SyncToRemoteServer(cfg)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 9 - 8
api/config/get.go

@@ -1,16 +1,17 @@
 package config
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+	"os"
+	"path/filepath"
+
 	"github.com/0xJacky/Nginx-UI/internal/config"
 	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
 	"github.com/sashabaranov/go-openai"
-	"net/http"
-	"os"
-	"path/filepath"
+	"github.com/uozi-tech/cosy"
 )
 
 type APIConfigResp struct {
@@ -32,20 +33,20 @@ func GetConfig(c *gin.Context) {
 
 	stat, err := os.Stat(absPath)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
 	content, err := os.ReadFile(absPath)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	q := query.Config
 	g := query.ChatGPTLog
 	chatgpt, err := g.Where(g.Name.Eq(absPath)).FirstOrCreate()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -55,7 +56,7 @@ func GetConfig(c *gin.Context) {
 
 	cfg, err := q.Where(q.Filepath.Eq(absPath)).FirstOrInit()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 6 - 5
api/config/list.go

@@ -1,14 +1,15 @@
 package config
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+	"os"
+	"strings"
+
 	"github.com/0xJacky/Nginx-UI/internal/config"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/gin-gonic/gin"
+	"github.com/uozi-tech/cosy"
 	"github.com/uozi-tech/cosy/logger"
-	"net/http"
-	"os"
-	"strings"
 )
 
 func GetConfigs(c *gin.Context) {
@@ -19,7 +20,7 @@ func GetConfigs(c *gin.Context) {
 
 	configFiles, err := os.ReadDir(nginx.GetConfPath(dir))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 4 - 4
api/config/mkdir.go

@@ -1,13 +1,13 @@
 package config
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+	"os"
+
 	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/gin-gonic/gin"
 	"github.com/uozi-tech/cosy"
-	"net/http"
-	"os"
 )
 
 func Mkdir(c *gin.Context) {
@@ -28,7 +28,7 @@ func Mkdir(c *gin.Context) {
 	}
 	err := os.Mkdir(fullPath, 0755)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 11 - 11
api/config/modify.go

@@ -1,7 +1,11 @@
 package config
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+	"os"
+	"path/filepath"
+	"time"
+
 	"github.com/0xJacky/Nginx-UI/internal/config"
 	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
@@ -11,10 +15,6 @@ import (
 	"github.com/sashabaranov/go-openai"
 	"github.com/uozi-tech/cosy"
 	"gorm.io/gen/field"
-	"net/http"
-	"os"
-	"path/filepath"
-	"time"
 )
 
 type EditConfigJson struct {
@@ -43,14 +43,14 @@ func EditConfig(c *gin.Context) {
 	content := json.Content
 	origContent, err := os.ReadFile(absPath)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
 	if content != "" && content != string(origContent) {
 		err = os.WriteFile(absPath, []byte(content), 0644)
 		if err != nil {
-			api.ErrHandler(c, err)
+			cosy.ErrHandler(c, err)
 			return
 		}
 	}
@@ -60,7 +60,7 @@ func EditConfig(c *gin.Context) {
 		Name: filepath.Base(absPath),
 	})).Where(q.Filepath.Eq(absPath)).FirstOrCreate()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -71,7 +71,7 @@ func EditConfig(c *gin.Context) {
 			SyncOverwrite: json.SyncOverwrite,
 		})
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -82,7 +82,7 @@ func EditConfig(c *gin.Context) {
 	g := query.ChatGPTLog
 	err = config.SyncToRemoteServer(cfg)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -96,7 +96,7 @@ func EditConfig(c *gin.Context) {
 
 	chatgpt, err := g.Where(g.Name.Eq(absPath)).FirstOrCreate()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 10 - 10
api/config/rename.go

@@ -1,7 +1,11 @@
 package config
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+
 	"github.com/0xJacky/Nginx-UI/internal/config"
 	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
@@ -9,10 +13,6 @@ import (
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
 	"github.com/uozi-tech/cosy"
-	"net/http"
-	"os"
-	"path/filepath"
-	"strings"
 )
 
 func Rename(c *gin.Context) {
@@ -45,7 +45,7 @@ func Rename(c *gin.Context) {
 
 	stat, err := os.Stat(origFullPath)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -58,7 +58,7 @@ func Rename(c *gin.Context) {
 
 	err = os.Rename(origFullPath, newFullPath)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -67,7 +67,7 @@ func Rename(c *gin.Context) {
 	q := query.Config
 	cfg, err := q.Where(q.Filepath.Eq(origFullPath)).FirstOrInit()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	if !stat.IsDir() {
@@ -85,14 +85,14 @@ func Rename(c *gin.Context) {
 		Name:     json.NewName,
 	})
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
 	if len(json.SyncNodeIds) > 0 {
 		err = config.SyncRenameOnRemoteServer(origFullPath, newFullPath, json.SyncNodeIds)
 		if err != nil {
-			api.ErrHandler(c, err)
+			cosy.ErrHandler(c, err)
 			return
 		}
 	}

+ 2 - 2
api/crypto/crypto.go

@@ -3,16 +3,16 @@ package crypto
 import (
 	"net/http"
 
-	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/crypto"
 	"github.com/gin-gonic/gin"
+	"github.com/uozi-tech/cosy"
 )
 
 // GetPublicKey generates a new ED25519 key pair and registers it in the cache
 func GetPublicKey(c *gin.Context) {
 	params, err := crypto.GetCryptoParams()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 5 - 5
api/nginx/nginx.go

@@ -1,11 +1,11 @@
 package nginx
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/gin-gonic/gin"
 	"github.com/uozi-tech/cosy"
-	"net/http"
 )
 
 func BuildNginxConfig(c *gin.Context) {
@@ -15,7 +15,7 @@ func BuildNginxConfig(c *gin.Context) {
 	}
 	content, err := ngxConf.BuildConfig()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	c.JSON(http.StatusOK, gin.H{
@@ -34,7 +34,7 @@ func TokenizeNginxConfig(c *gin.Context) {
 
 	ngxConfig, err := nginx.ParseNgxConfigByContent(json.Content)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	c.JSON(http.StatusOK, ngxConfig)
@@ -50,7 +50,7 @@ func FormatNginxConfig(c *gin.Context) {
 	}
 	content, err := nginx.FmtCode(json.Content)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	c.JSON(http.StatusOK, gin.H{

+ 5 - 5
api/notification/notification.go

@@ -1,13 +1,13 @@
 package notification
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
 	"github.com/spf13/cast"
 	"github.com/uozi-tech/cosy"
-	"net/http"
 )
 
 func Get(c *gin.Context) {
@@ -18,7 +18,7 @@ func Get(c *gin.Context) {
 	data, err := n.FirstByID(id)
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -39,14 +39,14 @@ func DestroyAll(c *gin.Context) {
 	err := db.Exec("DELETE FROM notifications").Error
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	// reset auto increment
 	err = db.Exec("UPDATE sqlite_sequence SET seq = 0 WHERE name = 'notifications';").Error
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 4 - 4
api/openai/store.go

@@ -1,13 +1,13 @@
 package openai
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
 	"github.com/sashabaranov/go-openai"
 	"github.com/uozi-tech/cosy"
-	"net/http"
 )
 
 func StoreChatGPTRecord(c *gin.Context) {
@@ -25,7 +25,7 @@ func StoreChatGPTRecord(c *gin.Context) {
 	_, err := g.Where(g.Name.Eq(name)).FirstOrCreate()
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -35,7 +35,7 @@ func StoreChatGPTRecord(c *gin.Context) {
 	})
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 3 - 3
api/settings/settings.go

@@ -2,14 +2,14 @@ package settings
 
 import (
 	"fmt"
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/internal/cron"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/settings"
 	"github.com/gin-gonic/gin"
 	"github.com/uozi-tech/cosy"
 	cSettings "github.com/uozi-tech/cosy/settings"
-	"net/http"
 )
 
 func GetServerName(c *gin.Context) {
@@ -87,7 +87,7 @@ func SaveSettings(c *gin.Context) {
 
 	err := settings.Save()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 4 - 4
api/sites/advance.go

@@ -1,12 +1,12 @@
 package sites
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
 	"github.com/uozi-tech/cosy"
-	"net/http"
 )
 
 func DomainEditByAdvancedMode(c *gin.Context) {
@@ -25,14 +25,14 @@ func DomainEditByAdvancedMode(c *gin.Context) {
 
 	_, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
 	_, err = s.Where(s.Path.Eq(path)).Update(s.Advanced, json.Advanced)
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 6 - 6
api/sites/auto_cert.go

@@ -1,13 +1,13 @@
 package sites
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/gin-gonic/gin"
 	"github.com/go-acme/lego/v4/certcrypto"
 	"github.com/uozi-tech/cosy"
-	"net/http"
 )
 
 func AddDomainToAutoCert(c *gin.Context) {
@@ -27,7 +27,7 @@ func AddDomainToAutoCert(c *gin.Context) {
 	certModel, err := model.FirstOrCreateCert(name, helper.GetKeyType(json.KeyType))
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -40,7 +40,7 @@ func AddDomainToAutoCert(c *gin.Context) {
 	})
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -52,7 +52,7 @@ func RemoveDomainFromAutoCert(c *gin.Context) {
 	certModel, err := model.FirstCert(name)
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -61,7 +61,7 @@ func RemoveDomainFromAutoCert(c *gin.Context) {
 	})
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	c.JSON(http.StatusOK, nil)

+ 3 - 3
api/sites/duplicate.go

@@ -1,11 +1,11 @@
 package sites
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/internal/site"
 	"github.com/gin-gonic/gin"
 	"github.com/uozi-tech/cosy"
-	"net/http"
 )
 
 func DuplicateSite(c *gin.Context) {
@@ -23,7 +23,7 @@ func DuplicateSite(c *gin.Context) {
 
 	err := site.Duplicate(src, json.Name)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 9 - 8
api/sites/list.go

@@ -1,7 +1,11 @@
 package sites
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+
 	"github.com/0xJacky/Nginx-UI/internal/config"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/model"
@@ -9,10 +13,7 @@ import (
 	"github.com/gin-gonic/gin"
 	"github.com/samber/lo"
 	"github.com/spf13/cast"
-	"net/http"
-	"os"
-	"path/filepath"
-	"strings"
+	"github.com/uozi-tech/cosy"
 )
 
 func GetSiteList(c *gin.Context) {
@@ -24,13 +25,13 @@ func GetSiteList(c *gin.Context) {
 
 	configFiles, err := os.ReadDir(nginx.GetConfPath("sites-available"))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
 	enabledConfig, err := os.ReadDir(nginx.GetConfPath("sites-enabled"))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -41,7 +42,7 @@ func GetSiteList(c *gin.Context) {
 	}
 	sites, err := sTx.Find()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	sitesMap := lo.SliceToMap(sites, func(item *model.Site) (string, *model.Site) {

+ 12 - 12
api/sites/site.go

@@ -1,7 +1,9 @@
 package sites
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+	"os"
+
 	"github.com/0xJacky/Nginx-UI/internal/cert"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/internal/site"
@@ -12,8 +14,6 @@ import (
 	"github.com/uozi-tech/cosy"
 	"github.com/uozi-tech/cosy/logger"
 	"gorm.io/gorm/clause"
-	"net/http"
-	"os"
 )
 
 func GetSite(c *gin.Context) {
@@ -36,7 +36,7 @@ func GetSite(c *gin.Context) {
 	g := query.ChatGPTLog
 	chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -47,7 +47,7 @@ func GetSite(c *gin.Context) {
 	s := query.Site
 	siteModel, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -59,7 +59,7 @@ func GetSite(c *gin.Context) {
 	if siteModel.Advanced {
 		origContent, err := os.ReadFile(path)
 		if err != nil {
-			api.ErrHandler(c, err)
+			cosy.ErrHandler(c, err)
 			return
 		}
 
@@ -78,7 +78,7 @@ func GetSite(c *gin.Context) {
 
 	nginxConfig, err := nginx.ParseNgxConfig(path)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -126,7 +126,7 @@ func SaveSite(c *gin.Context) {
 
 	err := site.Save(name, json.Content, json.Overwrite, json.SiteCategoryID, json.SyncNodeIDs)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -144,7 +144,7 @@ func RenameSite(c *gin.Context) {
 
 	err := site.Rename(oldName, json.NewName)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -156,7 +156,7 @@ func RenameSite(c *gin.Context) {
 func EnableSite(c *gin.Context) {
 	err := site.Enable(c.Param("name"))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -168,7 +168,7 @@ func EnableSite(c *gin.Context) {
 func DisableSite(c *gin.Context) {
 	err := site.Disable(c.Param("name"))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -180,7 +180,7 @@ func DisableSite(c *gin.Context) {
 func DeleteSite(c *gin.Context) {
 	err := site.Delete(c.Param("name"))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 2 - 3
api/streams/advance.go

@@ -3,7 +3,6 @@ package streams
 import (
 	"net/http"
 
-	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
@@ -26,14 +25,14 @@ func AdvancedEdit(c *gin.Context) {
 
 	_, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
 	_, err = s.Where(s.Path.Eq(path)).Update(s.Advanced, json.Advanced)
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 3 - 3
api/streams/duplicate.go

@@ -1,12 +1,12 @@
 package streams
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/gin-gonic/gin"
 	"github.com/uozi-tech/cosy"
-	"net/http"
 )
 
 func Duplicate(c *gin.Context) {
@@ -35,7 +35,7 @@ func Duplicate(c *gin.Context) {
 	_, err := helper.CopyFile(src, dst)
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 11 - 12
api/streams/streams.go

@@ -6,7 +6,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/config"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/internal/stream"
@@ -35,13 +34,13 @@ func GetStreams(c *gin.Context) {
 
 	configFiles, err := os.ReadDir(nginx.GetConfPath("streams-available"))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
 	enabledConfig, err := os.ReadDir(nginx.GetConfPath("streams-enabled"))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -98,7 +97,7 @@ func GetStream(c *gin.Context) {
 	chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -109,14 +108,14 @@ func GetStream(c *gin.Context) {
 	s := query.Stream
 	streamModel, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
 	if streamModel.Advanced {
 		origContent, err := os.ReadFile(path)
 		if err != nil {
-			api.ErrHandler(c, err)
+			cosy.ErrHandler(c, err)
 			return
 		}
 
@@ -136,7 +135,7 @@ func GetStream(c *gin.Context) {
 	nginxConfig, err := nginx.ParseNgxConfig(path)
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -168,7 +167,7 @@ func SaveStream(c *gin.Context) {
 
 	err := stream.Save(name, json.Content, json.Overwrite, json.SyncNodeIDs)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -178,7 +177,7 @@ func SaveStream(c *gin.Context) {
 func EnableStream(c *gin.Context) {
 	err := stream.Enable(c.Param("name"))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -190,7 +189,7 @@ func EnableStream(c *gin.Context) {
 func DisableStream(c *gin.Context) {
 	err := stream.Disable(c.Param("name"))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -202,7 +201,7 @@ func DisableStream(c *gin.Context) {
 func DeleteStream(c *gin.Context) {
 	err := stream.Delete(c.Param("name"))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -222,7 +221,7 @@ func RenameStream(c *gin.Context) {
 
 	err := stream.Rename(oldName, json.NewName)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 42 - 0
api/system/backup.go

@@ -0,0 +1,42 @@
+package system
+
+import (
+	"bytes"
+	"net/http"
+	"time"
+
+	"github.com/0xJacky/Nginx-UI/internal/backup"
+	"github.com/gin-gonic/gin"
+	"github.com/uozi-tech/cosy"
+)
+
+// CreateBackup creates a backup of nginx-ui and nginx configurations
+// and sends files directly for download
+func CreateBackup(c *gin.Context) {
+	result, err := backup.Backup()
+	if err != nil {
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	// Concatenate Key and IV
+	securityToken := result.AESKey + ":" + result.AESIv
+
+	// Prepare response content
+	reader := bytes.NewReader(result.BackupContent)
+	modTime := time.Now()
+
+	// Set HTTP headers for file download
+	fileName := result.BackupName
+	c.Header("Content-Description", "File Transfer")
+	c.Header("Content-Type", "application/zip")
+	c.Header("Content-Disposition", "attachment; filename="+fileName)
+	c.Header("Content-Transfer-Encoding", "binary")
+	c.Header("X-Backup-Security", securityToken) // Pass security token in header
+	c.Header("Expires", "0")
+	c.Header("Cache-Control", "must-revalidate")
+	c.Header("Pragma", "public")
+
+	// Send file content
+	http.ServeContent(c.Writer, c.Request, fileName, modTime, reader)
+}

+ 390 - 0
api/system/backup_test.go

@@ -0,0 +1,390 @@
+package system
+
+import (
+	"bytes"
+	"encoding/base64"
+	"encoding/json"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"net/http/httptest"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/0xJacky/Nginx-UI/internal/backup"
+	"github.com/0xJacky/Nginx-UI/settings"
+	"github.com/gin-gonic/gin"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/mock"
+	"github.com/uozi-tech/cosy/logger"
+	cosysettings "github.com/uozi-tech/cosy/settings"
+)
+
+// MockBackupService is used to mock the backup service
+type MockBackupService struct {
+	mock.Mock
+}
+
+func (m *MockBackupService) Backup() (backup.BackupResult, error) {
+	return backup.BackupResult{
+		BackupName:    "backup-test.zip",
+		AESKey:        "YWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXoxMjM0NTY=", // base64 encoded test key
+		AESIv:         "YWJjZGVmZ2hpamtsbW5vcA==",                     // base64 encoded test IV
+		BackupContent: []byte("test backup content"),
+	}, nil
+}
+
+func (m *MockBackupService) Restore(options backup.RestoreOptions) (backup.RestoreResult, error) {
+	return backup.RestoreResult{
+		RestoreDir:      options.RestoreDir,
+		NginxUIRestored: options.RestoreNginxUI,
+		NginxRestored:   options.RestoreNginx,
+		HashMatch:       options.VerifyHash,
+	}, nil
+}
+
+// MockedCreateBackup is a mocked version of CreateBackup that uses the mock service
+func MockedCreateBackup(c *gin.Context) {
+	mockService := &MockBackupService{}
+	result, err := mockService.Backup()
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"error": err.Error(),
+		})
+		return
+	}
+
+	// Concatenate Key and IV
+	securityToken := result.AESKey + ":" + result.AESIv
+
+	// Set HTTP headers for file download
+	fileName := result.BackupName
+	c.Header("Content-Description", "File Transfer")
+	c.Header("Content-Type", "application/zip")
+	c.Header("Content-Disposition", "attachment; filename="+fileName)
+	c.Header("Content-Transfer-Encoding", "binary")
+	c.Header("X-Backup-Security", securityToken) // Pass security token in header
+	c.Header("Expires", "0")
+	c.Header("Cache-Control", "must-revalidate")
+	c.Header("Pragma", "public")
+
+	// Send file content
+	c.Data(http.StatusOK, "application/zip", result.BackupContent)
+}
+
+// MockedRestoreBackup is a mocked version of RestoreBackup that uses the mock service
+func MockedRestoreBackup(c *gin.Context) {
+	// Get restore options
+	restoreNginx := c.PostForm("restore_nginx") == "true"
+	restoreNginxUI := c.PostForm("restore_nginx_ui") == "true"
+	verifyHash := c.PostForm("verify_hash") == "true"
+	securityToken := c.PostForm("security_token")
+
+	// Get backup file - we're just checking it exists for the test
+	_, err := c.FormFile("backup_file")
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"error": "Backup file not found",
+		})
+		return
+	}
+
+	// Validate security token
+	if securityToken == "" {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"error": "Invalid security token",
+		})
+		return
+	}
+
+	// Split security token to get Key and IV
+	parts := strings.Split(securityToken, ":")
+	if len(parts) != 2 {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"error": "Invalid security token format",
+		})
+		return
+	}
+
+	// Create temporary directory
+	tempDir, err := os.MkdirTemp("", "nginx-ui-restore-test-*")
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"error": "Failed to create temporary directory",
+		})
+		return
+	}
+
+	mockService := &MockBackupService{}
+	result, err := mockService.Restore(backup.RestoreOptions{
+		RestoreDir:     tempDir,
+		RestoreNginx:   restoreNginx,
+		RestoreNginxUI: restoreNginxUI,
+		VerifyHash:     verifyHash,
+	})
+
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"error": err.Error(),
+		})
+		return
+	}
+
+	c.JSON(http.StatusOK, RestoreResponse{
+		NginxUIRestored: result.NginxUIRestored,
+		NginxRestored:   result.NginxRestored,
+		HashMatch:       result.HashMatch,
+	})
+}
+
+func TestSetupEnvironment(t *testing.T) {
+	logger.Init(gin.DebugMode)
+	// Set up test environment
+	tempDir, err := os.MkdirTemp("", "nginx-ui-test-*")
+	assert.NoError(t, err)
+	defer os.RemoveAll(tempDir)
+
+	// Set up necessary directories and config files
+	nginxDir := filepath.Join(tempDir, "nginx")
+	configDir := filepath.Join(tempDir, "config")
+
+	err = os.MkdirAll(nginxDir, 0755)
+	assert.NoError(t, err)
+
+	err = os.MkdirAll(configDir, 0755)
+	assert.NoError(t, err)
+
+	// Create a config.ini file
+	configPath := filepath.Join(configDir, "config.ini")
+	err = os.WriteFile(configPath, []byte("[app]\nName = Nginx UI Test\n"), 0644)
+	assert.NoError(t, err)
+
+	// Create a database file
+	dbName := settings.DatabaseSettings.GetName()
+	dbPath := filepath.Join(configDir, dbName+".db")
+	err = os.WriteFile(dbPath, []byte("test database content"), 0644)
+	assert.NoError(t, err)
+
+	// Save original settings for restoration later
+	originalConfigDir := settings.NginxSettings.ConfigDir
+	originalConfPath := cosysettings.ConfPath
+
+	t.Logf("Original config path: %s", cosysettings.ConfPath)
+	t.Logf("Setting config path to: %s", configPath)
+
+	// Set the temporary directory as the Nginx config directory for testing
+	settings.NginxSettings.ConfigDir = nginxDir
+	cosysettings.ConfPath = configPath
+
+	t.Logf("Config path after setting: %s", cosysettings.ConfPath)
+
+	// Restore original settings after test
+	defer func() {
+		settings.NginxSettings.ConfigDir = originalConfigDir
+		cosysettings.ConfPath = originalConfPath
+	}()
+}
+
+func setupMockedRouter() *gin.Engine {
+	gin.SetMode(gin.TestMode)
+	r := gin.New()
+
+	// Setup router with mocked API endpoints to avoid environment issues
+	systemGroup := r.Group("/api/system")
+	systemGroup.POST("/backup", MockedCreateBackup)
+	systemGroup.POST("/backup/restore", MockedRestoreBackup)
+
+	return r
+}
+
+func TestCreateBackupAPI(t *testing.T) {
+	// Set up test environment
+	TestSetupEnvironment(t)
+
+	router := setupMockedRouter()
+	w := httptest.NewRecorder()
+	req, _ := http.NewRequest("POST", "/api/system/backup", nil)
+	router.ServeHTTP(w, req)
+
+	// If there's an error, it might be because the config path is empty
+	if w.Code != http.StatusOK {
+		var errorResponse map[string]interface{}
+		err := json.Unmarshal(w.Body.Bytes(), &errorResponse)
+		if err == nil {
+			t.Logf("Error response: %v", errorResponse)
+		}
+
+		// Skip the test if there's a configuration issue
+		if strings.Contains(w.Body.String(), "Config path is empty") {
+			t.Skip("Skipping test due to empty config path")
+			return
+		}
+	}
+
+	// Check response code - should be OK
+	assert.Equal(t, http.StatusOK, w.Code)
+
+	// Verify the backup API response
+	assert.Equal(t, "application/zip", w.Header().Get("Content-Type"))
+
+	// Check that Content-Disposition contains "attachment; filename=backup-"
+	contentDisposition := w.Header().Get("Content-Disposition")
+	assert.True(t, strings.HasPrefix(contentDisposition, "attachment; filename=backup-"),
+		"Content-Disposition should start with 'attachment; filename=backup-'")
+
+	assert.NotEmpty(t, w.Header().Get("X-Backup-Security"))
+	assert.NotEmpty(t, w.Body.Bytes())
+
+	// Verify security token format
+	securityToken := w.Header().Get("X-Backup-Security")
+	parts := bytes.Split([]byte(securityToken), []byte(":"))
+	assert.Equal(t, 2, len(parts))
+
+	// Verify key and IV can be decoded
+	key, err := base64.StdEncoding.DecodeString(string(parts[0]))
+	assert.NoError(t, err)
+	assert.Equal(t, 32, len(key))
+
+	iv, err := base64.StdEncoding.DecodeString(string(parts[1]))
+	assert.NoError(t, err)
+	assert.Equal(t, 16, len(iv))
+}
+
+func TestRestoreBackupAPI(t *testing.T) {
+	// Set up test environment
+	TestSetupEnvironment(t)
+
+	// First create a backup to restore
+	backupRouter := setupMockedRouter()
+	w1 := httptest.NewRecorder()
+	req1, _ := http.NewRequest("POST", "/api/system/backup", nil)
+	backupRouter.ServeHTTP(w1, req1)
+
+	// If there's an error creating the backup, skip the test
+	if w1.Code != http.StatusOK {
+		var errorResponse map[string]interface{}
+		err := json.Unmarshal(w1.Body.Bytes(), &errorResponse)
+		if err == nil {
+			t.Logf("Error response during backup creation: %v", errorResponse)
+		}
+		t.Skip("Skipping test due to backup creation failure")
+		return
+	}
+
+	assert.Equal(t, http.StatusOK, w1.Code)
+
+	// Get the security token from the backup response
+	securityToken := w1.Header().Get("X-Backup-Security")
+	assert.NotEmpty(t, securityToken)
+
+	// Get backup content
+	backupContent := w1.Body.Bytes()
+	assert.NotEmpty(t, backupContent)
+
+	// Setup temporary directory and save backup file
+	tempDir, err := os.MkdirTemp("", "restore-api-test-*")
+	assert.NoError(t, err)
+	defer os.RemoveAll(tempDir)
+
+	backupName := "backup-test.zip"
+	backupPath := filepath.Join(tempDir, backupName)
+	err = os.WriteFile(backupPath, backupContent, 0644)
+	assert.NoError(t, err)
+
+	// Setup router
+	router := setupMockedRouter()
+
+	// Create multipart form
+	body := new(bytes.Buffer)
+	writer := multipart.NewWriter(body)
+
+	// Add form fields
+	_ = writer.WriteField("restore_nginx", "false")
+	_ = writer.WriteField("restore_nginx_ui", "false")
+	_ = writer.WriteField("verify_hash", "true")
+	_ = writer.WriteField("security_token", securityToken)
+
+	// Add backup file
+	file, err := os.Open(backupPath)
+	assert.NoError(t, err)
+	defer file.Close()
+
+	part, err := writer.CreateFormFile("backup_file", backupName)
+	assert.NoError(t, err)
+
+	_, err = io.Copy(part, file)
+	assert.NoError(t, err)
+
+	err = writer.Close()
+	assert.NoError(t, err)
+
+	// Create request
+	w := httptest.NewRecorder()
+	req, _ := http.NewRequest("POST", "/api/system/backup/restore", body)
+	req.Header.Set("Content-Type", writer.FormDataContentType())
+
+	// Perform request
+	router.ServeHTTP(w, req)
+
+	// Check status code
+	t.Logf("Response: %s", w.Body.String())
+	assert.Equal(t, http.StatusOK, w.Code)
+
+	// Verify response structure
+	var response RestoreResponse
+	err = json.Unmarshal(w.Body.Bytes(), &response)
+	assert.NoError(t, err)
+
+	assert.Equal(t, false, response.NginxUIRestored)
+	assert.Equal(t, false, response.NginxRestored)
+	assert.Equal(t, true, response.HashMatch)
+}
+
+func TestRestoreBackupAPIErrors(t *testing.T) {
+	// Set up test environment
+	TestSetupEnvironment(t)
+
+	// Setup router
+	router := setupMockedRouter()
+
+	// Test case 1: Missing backup file
+	w1 := httptest.NewRecorder()
+	body1 := new(bytes.Buffer)
+	writer1 := multipart.NewWriter(body1)
+	_ = writer1.WriteField("security_token", "invalid:token")
+	writer1.Close()
+
+	req1, _ := http.NewRequest("POST", "/api/system/backup/restore", body1)
+	req1.Header.Set("Content-Type", writer1.FormDataContentType())
+
+	router.ServeHTTP(w1, req1)
+	assert.NotEqual(t, http.StatusOK, w1.Code)
+
+	// Test case 2: Invalid security token
+	w2 := httptest.NewRecorder()
+	body2 := new(bytes.Buffer)
+	writer2 := multipart.NewWriter(body2)
+	_ = writer2.WriteField("security_token", "invalidtoken") // No colon separator
+	writer2.Close()
+
+	req2, _ := http.NewRequest("POST", "/api/system/backup/restore", body2)
+	req2.Header.Set("Content-Type", writer2.FormDataContentType())
+
+	router.ServeHTTP(w2, req2)
+	assert.NotEqual(t, http.StatusOK, w2.Code)
+
+	// Test case 3: Invalid base64 encoding
+	w3 := httptest.NewRecorder()
+	body3 := new(bytes.Buffer)
+	writer3 := multipart.NewWriter(body3)
+	_ = writer3.WriteField("security_token", "invalid!base64:alsoinvalid!")
+	writer3.Close()
+
+	req3, _ := http.NewRequest("POST", "/api/system/backup/restore", body3)
+	req3.Header.Set("Content-Type", writer3.FormDataContentType())
+
+	router.ServeHTTP(w3, req3)
+	assert.NotEqual(t, http.StatusOK, w3.Code)
+}

+ 35 - 5
api/system/install.go

@@ -1,8 +1,11 @@
 package system
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+	"time"
+
 	"github.com/0xJacky/Nginx-UI/internal/kernel"
+	"github.com/0xJacky/Nginx-UI/internal/system"
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/0xJacky/Nginx-UI/settings"
@@ -11,16 +14,36 @@ import (
 	"github.com/uozi-tech/cosy"
 	cSettings "github.com/uozi-tech/cosy/settings"
 	"golang.org/x/crypto/bcrypt"
-	"net/http"
 )
 
+// System startup time
+var startupTime time.Time
+
+func init() {
+	// Record system startup time
+	startupTime = time.Now()
+}
+
 func installLockStatus() bool {
 	return settings.NodeSettings.SkipInstallation || "" != cSettings.AppSettings.JwtSecret
 }
 
+// Check if installation time limit (10 minutes) is exceeded
+func isInstallTimeoutExceeded() bool {
+	return time.Since(startupTime) > 10*time.Minute
+}
+
 func InstallLockCheck(c *gin.Context) {
+	locked := installLockStatus()
+	timeout := false
+
+	if !locked {
+		timeout = isInstallTimeoutExceeded()
+	}
+
 	c.JSON(http.StatusOK, gin.H{
-		"lock": installLockStatus(),
+		"lock":    locked,
+		"timeout": timeout,
 	})
 }
 
@@ -39,6 +62,13 @@ func InstallNginxUI(c *gin.Context) {
 		})
 		return
 	}
+
+	// Check if installation time limit (10 minutes) is exceeded
+	if isInstallTimeoutExceeded() {
+		cosy.ErrHandler(c, system.ErrInstallTimeout)
+		return
+	}
+
 	var json InstallJson
 	ok := cosy.BindAndValid(c, &json)
 	if !ok {
@@ -54,7 +84,7 @@ func InstallNginxUI(c *gin.Context) {
 
 	err := settings.Save()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -70,7 +100,7 @@ func InstallNginxUI(c *gin.Context) {
 	})
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 135 - 0
api/system/restore.go

@@ -0,0 +1,135 @@
+package system
+
+import (
+	"encoding/base64"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/0xJacky/Nginx-UI/internal/backup"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"github.com/jpillora/overseer"
+	"github.com/uozi-tech/cosy"
+)
+
+// RestoreResponse contains the response data for restore operation
+type RestoreResponse struct {
+	NginxUIRestored bool `json:"nginx_ui_restored"`
+	NginxRestored   bool `json:"nginx_restored"`
+	HashMatch       bool `json:"hash_match"`
+}
+
+// RestoreBackup restores from uploaded backup and security info
+func RestoreBackup(c *gin.Context) {
+	// Get restore options
+	restoreNginx := c.PostForm("restore_nginx") == "true"
+	restoreNginxUI := c.PostForm("restore_nginx_ui") == "true"
+	verifyHash := c.PostForm("verify_hash") == "true"
+	securityToken := c.PostForm("security_token") // Get concatenated key and IV
+	// Get backup file
+	backupFile, err := c.FormFile("backup_file")
+	if err != nil {
+		cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrBackupFileNotFound, err.Error()))
+		return
+	}
+
+	// Validate security token
+	if securityToken == "" {
+		cosy.ErrHandler(c, backup.ErrInvalidSecurityToken)
+		return
+	}
+
+	// Split security token to get Key and IV
+	parts := strings.Split(securityToken, ":")
+	if len(parts) != 2 {
+		cosy.ErrHandler(c, backup.ErrInvalidSecurityToken)
+		return
+	}
+
+	aesKey := parts[0]
+	aesIv := parts[1]
+
+	// Decode Key and IV from base64
+	key, err := base64.StdEncoding.DecodeString(aesKey)
+	if err != nil {
+		cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrInvalidAESKey, err.Error()))
+		return
+	}
+
+	iv, err := base64.StdEncoding.DecodeString(aesIv)
+	if err != nil {
+		cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrInvalidAESIV, err.Error()))
+		return
+	}
+
+	// Create temporary directory for files
+	tempDir, err := os.MkdirTemp("", "nginx-ui-restore-upload-*")
+	if err != nil {
+		cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrCreateTempDir, err.Error()))
+		return
+	}
+	defer os.RemoveAll(tempDir)
+
+	// Save backup file
+	backupPath := filepath.Join(tempDir, backupFile.Filename)
+	if err := c.SaveUploadedFile(backupFile, backupPath); err != nil {
+		cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrCreateBackupFile, err.Error()))
+		return
+	}
+
+	// Create temporary directory for restore operation
+	restoreDir, err := os.MkdirTemp("", "nginx-ui-restore-*")
+	if err != nil {
+		cosy.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrCreateRestoreDir, err.Error()))
+		return
+	}
+
+	// Set restore options
+	options := backup.RestoreOptions{
+		BackupPath:     backupPath,
+		AESKey:         key,
+		AESIv:          iv,
+		RestoreDir:     restoreDir,
+		RestoreNginx:   restoreNginx,
+		RestoreNginxUI: restoreNginxUI,
+		VerifyHash:     verifyHash,
+	}
+
+	// Perform restore
+	result, err := backup.Restore(options)
+	if err != nil {
+		// Clean up temporary directory on error
+		os.RemoveAll(restoreDir)
+		cosy.ErrHandler(c, err)
+		return
+	}
+
+	// If not actually restoring anything, clean up directory to avoid disk space waste
+	if !restoreNginx && !restoreNginxUI {
+		defer os.RemoveAll(restoreDir)
+	}
+
+	if restoreNginx {
+		go func() {
+			time.Sleep(2 * time.Second)
+			nginx.Restart()
+		}()
+	}
+
+	if restoreNginxUI {
+		go func() {
+			time.Sleep(2 * time.Second)
+			// gracefully restart
+			overseer.Restart()
+		}()
+	}
+
+	c.JSON(http.StatusOK, RestoreResponse{
+		NginxUIRestored: result.NginxUIRestored,
+		NginxRestored:   result.NginxRestored,
+		HashMatch:       result.HashMatch,
+	})
+}

+ 17 - 0
api/system/router.go

@@ -16,6 +16,23 @@ func InitPrivateRouter(r *gin.RouterGroup) {
 	r.GET("upgrade/current", GetCurrentVersion)
 	r.GET("self_check", SelfCheck)
 	r.POST("self_check/:name/fix", SelfCheckFix)
+
+	// Backup endpoint only
+	r.GET("system/backup", CreateBackup)
+}
+
+func InitBackupRestoreRouter(r *gin.RouterGroup) {
+	r.POST("system/backup/restore",
+		func(ctx *gin.Context) {
+			// If system is installed, verify user authentication
+			if installLockStatus() {
+				middleware.AuthRequired()(ctx)
+			} else {
+				ctx.Next()
+			}
+		},
+		middleware.EncryptedForm(),
+		RestoreBackup)
 }
 
 func InitWebSocketRouter(r *gin.RouterGroup) {

+ 3 - 3
api/system/upgrade.go

@@ -4,24 +4,24 @@ import (
 	"net/http"
 	"os"
 
-	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/upgrader"
 	"github.com/0xJacky/Nginx-UI/internal/version"
 	"github.com/0xJacky/Nginx-UI/settings"
 	"github.com/gin-gonic/gin"
 	"github.com/gorilla/websocket"
+	"github.com/uozi-tech/cosy"
 	"github.com/uozi-tech/cosy/logger"
 )
 
 func GetRelease(c *gin.Context) {
 	data, err := upgrader.GetRelease(c.Query("channel"))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	runtimeInfo, err := upgrader.GetRuntimeInfo()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	type resp struct {

+ 8 - 6
api/template/template.go

@@ -1,11 +1,13 @@
 package template
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/internal/template"
 	"github.com/gin-gonic/gin"
-	"net/http"
+
+	"github.com/uozi-tech/cosy"
 )
 
 func GetDefaultSiteTemplate(c *gin.Context) {
@@ -40,7 +42,7 @@ func GetDefaultSiteTemplate(c *gin.Context) {
 
 	content, err := ngxConfig.BuildConfig()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -55,7 +57,7 @@ func GetTemplateConfList(c *gin.Context) {
 	configList, err := template.GetTemplateList("conf")
 
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -67,7 +69,7 @@ func GetTemplateConfList(c *gin.Context) {
 func GetTemplateBlockList(c *gin.Context) {
 	configList, err := template.GetTemplateList("block")
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -91,7 +93,7 @@ func GetTemplateBlock(c *gin.Context) {
 
 	detail, err := template.ParseTemplate("block", c.Param("name"), bindData)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	info.Variables = bindData

+ 8 - 8
api/user/2fa.go

@@ -81,17 +81,17 @@ func Start2FASecureSessionByOTP(c *gin.Context) {
 	}
 	u := api.CurrentUser(c)
 	if !u.EnabledOTP() {
-		api.ErrHandler(c, user.ErrUserNotEnabledOTPAs2FA)
+		cosy.ErrHandler(c, user.ErrUserNotEnabledOTPAs2FA)
 		return
 	}
 
 	if json.OTP == "" && json.RecoveryCode == "" {
-		api.ErrHandler(c, user.ErrOTPOrRecoveryCodeEmpty)
+		cosy.ErrHandler(c, user.ErrOTPOrRecoveryCodeEmpty)
 		return
 	}
 
 	if err := user.VerifyOTP(u, json.OTP, json.RecoveryCode); err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -104,14 +104,14 @@ func Start2FASecureSessionByOTP(c *gin.Context) {
 
 func BeginStart2FASecureSessionByPasskey(c *gin.Context) {
 	if !passkey.Enabled() {
-		api.ErrHandler(c, user.ErrWebAuthnNotConfigured)
+		cosy.ErrHandler(c, user.ErrWebAuthnNotConfigured)
 		return
 	}
 	webauthnInstance := passkey.GetInstance()
 	u := api.CurrentUser(c)
 	options, sessionData, err := webauthnInstance.BeginLogin(u)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	passkeySessionID := uuid.NewString()
@@ -124,13 +124,13 @@ func BeginStart2FASecureSessionByPasskey(c *gin.Context) {
 
 func FinishStart2FASecureSessionByPasskey(c *gin.Context) {
 	if !passkey.Enabled() {
-		api.ErrHandler(c, user.ErrWebAuthnNotConfigured)
+		cosy.ErrHandler(c, user.ErrWebAuthnNotConfigured)
 		return
 	}
 	passkeySessionID := c.GetHeader("X-Passkey-Session-ID")
 	sessionDataBytes, ok := cache.Get(passkeySessionID)
 	if !ok {
-		api.ErrHandler(c, user.ErrSessionNotFound)
+		cosy.ErrHandler(c, user.ErrSessionNotFound)
 		return
 	}
 	sessionData := sessionDataBytes.(*webauthn.SessionData)
@@ -138,7 +138,7 @@ func FinishStart2FASecureSessionByPasskey(c *gin.Context) {
 	u := api.CurrentUser(c)
 	credential, err := webauthnInstance.FinishLogin(u, *sessionData, c.Request)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	rawID := strings.TrimRight(base64.StdEncoding.EncodeToString(credential.ID), "=")

+ 7 - 7
api/user/auth.go

@@ -1,18 +1,18 @@
 package user
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
+	"errors"
+	"math/rand/v2"
+	"net/http"
+	"sync"
+	"time"
+
 	"github.com/0xJacky/Nginx-UI/internal/user"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/0xJacky/Nginx-UI/settings"
 	"github.com/gin-gonic/gin"
-	"errors"
 	"github.com/uozi-tech/cosy"
 	"github.com/uozi-tech/cosy/logger"
-	"math/rand/v2"
-	"net/http"
-	"sync"
-	"time"
 )
 
 var mutex = &sync.Mutex{}
@@ -75,7 +75,7 @@ func Login(c *gin.Context) {
 		case errors.Is(err, user.ErrUserBanned):
 			c.JSON(http.StatusForbidden, user.ErrUserBanned)
 		default:
-			api.ErrHandler(c, err)
+			cosy.ErrHandler(c, err)
 		}
 		user.BanIP(clientIP)
 		return

+ 10 - 10
api/user/casdoor.go

@@ -1,18 +1,18 @@
 package user
 
 import (
+	"errors"
 	"fmt"
-	"github.com/0xJacky/Nginx-UI/api"
+	"net/http"
+	"net/url"
+	"os"
+
 	"github.com/0xJacky/Nginx-UI/internal/user"
 	"github.com/0xJacky/Nginx-UI/settings"
 	"github.com/casdoor/casdoor-go-sdk/casdoorsdk"
 	"github.com/gin-gonic/gin"
-	"errors"
 	"github.com/uozi-tech/cosy"
 	"gorm.io/gorm"
-	"net/http"
-	"net/url"
-	"os"
 )
 
 type CasdoorLoginUser struct {
@@ -44,7 +44,7 @@ func CasdoorCallback(c *gin.Context) {
 
 	certBytes, err := os.ReadFile(certificatePath)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -52,13 +52,13 @@ func CasdoorCallback(c *gin.Context) {
 
 	token, err := casdoorsdk.GetOAuthToken(loginUser.Code, loginUser.State)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
 	claims, err := casdoorsdk.ParseJwtToken(token.AccessToken)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -69,14 +69,14 @@ func CasdoorCallback(c *gin.Context) {
 				"message": "User not exist",
 			})
 		} else {
-			api.ErrHandler(c, err)
+			cosy.ErrHandler(c, err)
 		}
 		return
 	}
 
 	userToken, err := user.GenerateJWT(u)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 5 - 5
api/user/otp.go

@@ -32,7 +32,7 @@ func GenerateTOTP(c *gin.Context) {
 	}
 	otpKey, err := totp.Generate(otpOpts)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -75,14 +75,14 @@ func EnrollTOTP(c *gin.Context) {
 
 	ciphertext, err := crypto.AesEncrypt([]byte(twoFA.Secret))
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.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)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -91,7 +91,7 @@ func EnrollTOTP(c *gin.Context) {
 	cUser.RecoveryCodes = recoveryCodes
 	_, err = u.Where(u.ID.Eq(cUser.ID)).Updates(cUser)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -106,7 +106,7 @@ func ResetOTP(c *gin.Context) {
 	u := query.User
 	_, err := u.Where(u.ID.Eq(cUser.ID)).UpdateSimple(u.OTPSecret.Null(), u.RecoveryCodes.Null())
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 10 - 10
api/user/passkey.go

@@ -40,7 +40,7 @@ func BeginPasskeyRegistration(c *gin.Context) {
 
 	options, sessionData, err := webauthnInstance.BeginRegistration(u)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	cache.Set(buildCachePasskeyRegKey(u.ID), sessionData, passkeyTimeout)
@@ -53,14 +53,14 @@ func FinishPasskeyRegistration(c *gin.Context) {
 	webauthnInstance := passkey.GetInstance()
 	sessionDataBytes, ok := cache.Get(buildCachePasskeyRegKey(cUser.ID))
 	if !ok {
-		api.ErrHandler(c, user.ErrSessionNotFound)
+		cosy.ErrHandler(c, user.ErrSessionNotFound)
 		return
 	}
 
 	sessionData := sessionDataBytes.(*webauthn.SessionData)
 	credential, err := webauthnInstance.FinishRegistration(cUser, *sessionData, c.Request)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	cache.Del(buildCachePasskeyRegKey(cUser.ID))
@@ -76,7 +76,7 @@ func FinishPasskeyRegistration(c *gin.Context) {
 		LastUsedAt: time.Now().Unix(),
 	})
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -87,13 +87,13 @@ func FinishPasskeyRegistration(c *gin.Context) {
 
 func BeginPasskeyLogin(c *gin.Context) {
 	if !passkey.Enabled() {
-		api.ErrHandler(c, user.ErrWebAuthnNotConfigured)
+		cosy.ErrHandler(c, user.ErrWebAuthnNotConfigured)
 		return
 	}
 	webauthnInstance := passkey.GetInstance()
 	options, sessionData, err := webauthnInstance.BeginDiscoverableLogin()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 	sessionID := uuid.NewString()
@@ -107,13 +107,13 @@ func BeginPasskeyLogin(c *gin.Context) {
 
 func FinishPasskeyLogin(c *gin.Context) {
 	if !passkey.Enabled() {
-		api.ErrHandler(c, user.ErrWebAuthnNotConfigured)
+		cosy.ErrHandler(c, user.ErrWebAuthnNotConfigured)
 		return
 	}
 	sessionId := c.GetHeader("X-Passkey-Session-ID")
 	sessionDataBytes, ok := cache.Get(sessionId)
 	if !ok {
-		api.ErrHandler(c, user.ErrSessionNotFound)
+		cosy.ErrHandler(c, user.ErrSessionNotFound)
 		return
 	}
 	webauthnInstance := passkey.GetInstance()
@@ -134,7 +134,7 @@ func FinishPasskeyLogin(c *gin.Context) {
 			return outUser, err
 		}, *sessionData, c.Request)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -167,7 +167,7 @@ func GetPasskeyList(c *gin.Context) {
 	p := query.Passkey
 	passkeys, err := p.Where(p.UserID.Eq(u.ID)).Find()
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 3 - 2
api/user/recovery.go

@@ -10,6 +10,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
+	"github.com/uozi-tech/cosy"
 )
 
 type RecoveryCodesResponse struct {
@@ -41,7 +42,7 @@ func ViewRecoveryCodes(c *gin.Context) {
 	user.RecoveryCodes.LastViewed = &t
 	_, err := u.Where(u.ID.Eq(user.ID)).Updates(user)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 
@@ -61,7 +62,7 @@ func GenerateRecoveryCodes(c *gin.Context) {
 	u := query.User
 	_, err := u.Where(u.ID.Eq(user.ID)).Updates(user)
 	if err != nil {
-		api.ErrHandler(c, err)
+		cosy.ErrHandler(c, err)
 		return
 	}
 

+ 2 - 0
app/components.d.ts

@@ -70,6 +70,7 @@ declare module 'vue' {
     ATag: typeof import('ant-design-vue/es')['Tag']
     ATextarea: typeof import('ant-design-vue/es')['Textarea']
     ATooltip: typeof import('ant-design-vue/es')['Tooltip']
+    AUploadDragger: typeof import('ant-design-vue/es')['UploadDragger']
     BreadcrumbBreadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
     ChartAreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default']
     ChartRadialBarChart: typeof import('./src/components/Chart/RadialBarChart.vue')['default']
@@ -105,6 +106,7 @@ declare module 'vue' {
     SwitchAppearanceIconsVPIconMoon: typeof import('./src/components/SwitchAppearance/icons/VPIconMoon.vue')['default']
     SwitchAppearanceIconsVPIconSun: typeof import('./src/components/SwitchAppearance/icons/VPIconSun.vue')['default']
     SwitchAppearanceSwitchAppearance: typeof import('./src/components/SwitchAppearance/SwitchAppearance.vue')['default']
+    SystemRestoreSystemRestoreContent: typeof import('./src/components/SystemRestore/SystemRestoreContent.vue')['default']
     TwoFAAuthorization: typeof import('./src/components/TwoFA/Authorization.vue')['default']
     VPSwitchVPSwitch: typeof import('./src/components/VPSwitch/VPSwitch.vue')['default']
   }

+ 57 - 0
app/src/api/backup.ts

@@ -0,0 +1,57 @@
+import http from '@/lib/http'
+
+/**
+ * Interface for restore backup response
+ */
+export interface RestoreResponse {
+  restore_dir: string
+  nginx_ui_restored: boolean
+  nginx_restored: boolean
+  hash_match: boolean
+}
+
+/**
+ * Interface for restore backup options
+ */
+export interface RestoreOptions {
+  backup_file: File
+  security_token: string
+  restore_nginx: boolean
+  restore_nginx_ui: boolean
+  verify_hash: boolean
+}
+
+const backup = {
+  /**
+   * Create and download a backup of nginx-ui and nginx configurations
+   * Use http module with returnFullResponse option to access headers
+   */
+  createBackup() {
+    return http.get('/system/backup', {
+      responseType: 'blob',
+      returnFullResponse: true,
+    })
+  },
+
+  /**
+   * Restore from a backup file
+   * @param options RestoreOptions
+   */
+  restoreBackup(options: RestoreOptions) {
+    const formData = new FormData()
+    formData.append('backup_file', options.backup_file)
+    formData.append('security_token', options.security_token)
+    formData.append('restore_nginx', options.restore_nginx.toString())
+    formData.append('restore_nginx_ui', options.restore_nginx_ui.toString())
+    formData.append('verify_hash', options.verify_hash.toString())
+
+    return http.post('/system/backup/restore', formData, {
+      headers: {
+        'Content-Type': 'multipart/form-data;charset=UTF-8',
+      },
+      crypto: true,
+    })
+  },
+}
+
+export default backup

+ 6 - 1
app/src/api/install.ts

@@ -7,9 +7,14 @@ export interface InstallRequest {
   database: string
 }
 
+export interface InstallLockResponse {
+  lock: boolean
+  timeout: boolean
+}
+
 const install = {
   get_lock() {
-    return http.get('/install')
+    return http.get<InstallLockResponse>('/install')
   },
   install_nginx_ui(data: InstallRequest) {
     return http.post('/install', data, { crypto: true })

+ 355 - 0
app/src/components/SystemRestore/SystemRestoreContent.vue

@@ -0,0 +1,355 @@
+<script setup lang="ts">
+import type { RestoreOptions, RestoreResponse } from '@/api/backup'
+import type { UploadFile } from 'ant-design-vue'
+import backup from '@/api/backup'
+import { InboxOutlined } from '@ant-design/icons-vue'
+import { message } from 'ant-design-vue'
+
+// Define props using TypeScript interface
+interface SystemRestoreProps {
+  showTitle?: boolean
+  showNginxOptions?: boolean
+}
+
+// Define emits using TypeScript interface
+interface SystemRestoreEmits {
+  (e: 'restoreSuccess', options: { restoreNginx: boolean, restoreNginxUI: boolean }): void
+  (e: 'restoreError', error: Error): void
+}
+
+withDefaults(defineProps<SystemRestoreProps>(), {
+  showTitle: true,
+  showNginxOptions: true,
+})
+
+const emit = defineEmits<SystemRestoreEmits>()
+
+// Use UploadFile from ant-design-vue
+const uploadFiles = ref<UploadFile[]>([])
+const isRestoring = ref(false)
+
+const formModel = reactive({
+  securityToken: '',
+  restoreNginx: true,
+  restoreNginxUI: true,
+  verifyHash: true,
+})
+
+// 添加两个变量控制模态框显示和倒计时
+const showRestoreModal = ref(false)
+const countdown = ref(5)
+const countdownTimer = ref<ReturnType<typeof setInterval> | null>(null)
+
+// Reset countdown function
+function resetCountdown() {
+  countdown.value = 5
+  showRestoreModal.value = true
+
+  // Clear any existing timer
+  if (countdownTimer.value) {
+    clearInterval(countdownTimer.value)
+  }
+
+  // Start countdown timer
+  countdownTimer.value = setInterval(() => {
+    countdown.value--
+    if (countdown.value <= 0 && countdownTimer.value) {
+      clearInterval(countdownTimer.value)
+    }
+  }, 1000)
+}
+
+// Handle OK button click
+function handleModalOk() {
+  if (countdownTimer.value) {
+    clearInterval(countdownTimer.value)
+  }
+  // Emit success event with restore options
+  emit('restoreSuccess', {
+    restoreNginx: formModel.restoreNginx,
+    restoreNginxUI: formModel.restoreNginxUI,
+  })
+}
+
+function handleBeforeUpload(file: File) {
+  // Check if file type is zip
+  const isZip = file.name.toLowerCase().endsWith('.zip')
+  if (!isZip) {
+    message.error($gettext('Only zip files are allowed'))
+    uploadFiles.value = []
+    return
+  }
+
+  // Create UploadFile object and directly manage uploadFiles
+  const uploadFile = {
+    uid: Date.now().toString(),
+    name: file.name,
+    status: 'done',
+    size: file.size,
+    type: file.type,
+    originFileObj: file,
+  } as UploadFile
+
+  // Keep only the current file
+  uploadFiles.value = [uploadFile]
+
+  // Prevent default upload behavior
+  return false
+}
+
+// Handle file removal
+function handleRemove() {
+  uploadFiles.value = []
+}
+
+async function doRestore() {
+  if (uploadFiles.value.length === 0) {
+    message.warning($gettext('Please select a backup file'))
+    return
+  }
+
+  if (!formModel.securityToken) {
+    message.warning($gettext('Please enter the security token'))
+    return
+  }
+
+  try {
+    isRestoring.value = true
+
+    const uploadedFile = uploadFiles.value[0]
+    if (!uploadedFile.originFileObj) {
+      message.error($gettext('Invalid file object'))
+      return
+    }
+
+    const options: RestoreOptions = {
+      backup_file: uploadedFile.originFileObj,
+      security_token: formModel.securityToken,
+      restore_nginx: formModel.restoreNginx,
+      restore_nginx_ui: formModel.restoreNginxUI,
+      verify_hash: formModel.verifyHash,
+    }
+
+    const data = await backup.restoreBackup(options) as RestoreResponse
+
+    message.success($gettext('Restore completed successfully'))
+
+    if (data.nginx_restored) {
+      message.info($gettext('Nginx configuration has been restored'))
+    }
+
+    if (data.nginx_ui_restored) {
+      message.info($gettext('Nginx UI configuration has been restored'))
+      // If UI was restored, show the countdown modal
+      resetCountdown()
+    }
+    else {
+      // If UI was not restored, emit success event directly
+      emit('restoreSuccess', {
+        restoreNginx: formModel.restoreNginx,
+        restoreNginxUI: formModel.restoreNginxUI,
+      })
+    }
+
+    if (data.hash_match === false && formModel.verifyHash) {
+      message.warning($gettext('Backup file integrity check failed, it may have been tampered with'))
+    }
+
+    // Reset form after successful restore
+    uploadFiles.value = []
+    formModel.securityToken = ''
+  }
+  catch (error) {
+    console.error('Restore failed:', error)
+    emit('restoreError', error instanceof Error ? error : new Error(String(error)))
+  }
+  finally {
+    isRestoring.value = false
+  }
+}
+</script>
+
+<template>
+  <div>
+    <ACard v-if="showTitle" :title="$gettext('System Restore')" :bordered="false">
+      <AAlert
+        show-icon
+        type="warning"
+        :message="$gettext('Warning: Restore operation will overwrite current configurations. Make sure you have a valid backup file and security token, and carefully select what to restore.')"
+        class="mb-4"
+      />
+
+      <AUploadDragger
+        :file-list="uploadFiles"
+        :multiple="false"
+        :max-count="1"
+        accept=".zip"
+        :before-upload="handleBeforeUpload"
+        @remove="handleRemove"
+      >
+        <p class="ant-upload-drag-icon">
+          <InboxOutlined />
+        </p>
+        <p class="ant-upload-text">
+          {{ $gettext('Click or drag backup file to this area to upload') }}
+        </p>
+        <p class="ant-upload-hint">
+          {{ $gettext('Supported file type: .zip') }}
+        </p>
+      </AUploadDragger>
+
+      <AForm
+        v-if="uploadFiles.length > 0"
+        :model="formModel"
+        layout="vertical"
+        class="mt-4"
+      >
+        <AFormItem :label="$gettext('Security Token')">
+          <AInput
+            v-model:value="formModel.securityToken"
+            :placeholder="$gettext('Please enter the security token received during backup')"
+          />
+        </AFormItem>
+
+        <AFormItem>
+          <ACheckbox v-model:checked="formModel.verifyHash" :disabled="true">
+            {{ $gettext('Verify Backup File Integrity') }}
+          </ACheckbox>
+        </AFormItem>
+
+        <template v-if="showNginxOptions">
+          <AFormItem>
+            <ACheckbox v-model:checked="formModel.restoreNginx">
+              {{ $gettext('Restore Nginx Configuration') }}
+            </ACheckbox>
+            <div class="text-gray-500 ml-6 mt-1 text-sm">
+              <p class="mb-0">
+                {{ $gettext('This will restore all Nginx configuration files. Nginx will restart after the restoration is complete.') }}
+              </p>
+            </div>
+          </AFormItem>
+
+          <AFormItem>
+            <ACheckbox v-model:checked="formModel.restoreNginxUI">
+              {{ $gettext('Restore Nginx UI Configuration') }}
+            </ACheckbox>
+            <div class="text-gray-500 ml-6 mt-1 text-sm">
+              <p class="mb-0">
+                {{ $gettext('This will restore configuration files and database. Nginx UI will restart after the restoration is complete.') }}
+              </p>
+            </div>
+          </AFormItem>
+        </template>
+
+        <AFormItem>
+          <AButton type="primary" :loading="isRestoring" @click="doRestore">
+            {{ $gettext('Start Restore') }}
+          </AButton>
+        </AFormItem>
+      </AForm>
+    </ACard>
+    <div v-else>
+      <AAlert
+        show-icon
+        type="warning"
+        :message="$gettext('Warning: Restore operation will overwrite current configurations. Make sure you have a valid backup file and security token, and carefully select what to restore.')"
+        class="mb-4"
+      />
+
+      <AUploadDragger
+        :file-list="uploadFiles"
+        :multiple="false"
+        :max-count="1"
+        accept=".zip"
+        :before-upload="handleBeforeUpload"
+        @remove="handleRemove"
+      >
+        <p class="ant-upload-drag-icon">
+          <InboxOutlined />
+        </p>
+        <p class="ant-upload-text">
+          {{ $gettext('Click or drag backup file to this area to upload') }}
+        </p>
+        <p class="ant-upload-hint">
+          {{ $gettext('Supported file type: .zip') }}
+        </p>
+      </AUploadDragger>
+
+      <AForm
+        v-if="uploadFiles.length > 0"
+        :model="formModel"
+        layout="vertical"
+        class="mt-4"
+      >
+        <AFormItem :label="$gettext('Security Token')">
+          <AInput
+            v-model:value="formModel.securityToken"
+            :placeholder="$gettext('Please enter the security token received during backup')"
+          />
+        </AFormItem>
+
+        <AFormItem>
+          <ACheckbox v-model:checked="formModel.verifyHash" :disabled="true">
+            {{ $gettext('Verify Backup File Integrity') }}
+          </ACheckbox>
+        </AFormItem>
+
+        <template v-if="showNginxOptions">
+          <AFormItem>
+            <ACheckbox v-model:checked="formModel.restoreNginx">
+              {{ $gettext('Restore Nginx Configuration') }}
+            </ACheckbox>
+            <div class="text-gray-500 ml-6 mt-1 text-sm">
+              <p class="mb-0">
+                {{ $gettext('This will restore all Nginx configuration files. Nginx will restart after the restoration is complete.') }}
+              </p>
+            </div>
+          </AFormItem>
+
+          <AFormItem>
+            <ACheckbox v-model:checked="formModel.restoreNginxUI">
+              {{ $gettext('Restore Nginx UI Configuration') }}
+            </ACheckbox>
+            <div class="text-gray-500 ml-6 mt-1 text-sm">
+              <p class="mb-0">
+                {{ $gettext('This will restore configuration files and database. Nginx UI will restart after the restoration is complete.') }}
+              </p>
+            </div>
+          </AFormItem>
+        </template>
+
+        <AFormItem>
+          <AButton type="primary" :loading="isRestoring" @click="doRestore">
+            {{ $gettext('Start Restore') }}
+          </AButton>
+        </AFormItem>
+      </AForm>
+    </div>
+
+    <!-- Modal for countdown -->
+    <AModal
+      v-model:open="showRestoreModal"
+      :title="$gettext('Automatic Restart')"
+      :mask-closable="false"
+    >
+      <p>
+        {{ $gettext('Nginx UI configuration has been restored and will restart automatically in a few seconds.') }}
+      </p>
+      <p v-if="countdown > 0">
+        {{ $gettext('You can close this dialog in %{countdown} seconds', { countdown: countdown.toString() }) }}
+      </p>
+      <p v-else>
+        {{ $gettext('You can close this dialog now') }}
+      </p>
+      <template #footer>
+        <AButton
+          type="primary"
+          :disabled="countdown > 0"
+          @click="handleModalOk"
+        >
+          {{ countdown > 0 ? `OK (${countdown}s)` : 'OK' }}
+        </AButton>
+      </template>
+    </AModal>
+  </div>
+</template>

+ 60 - 0
app/src/constants/errors/backup.ts

@@ -0,0 +1,60 @@
+export default {
+  4002: () => $gettext('Failed to create temporary directory'),
+  4003: () => $gettext('Failed to create temporary subdirectory'),
+  4004: () => $gettext('Failed to backup Nginx UI files: {0}'),
+  4005: () => $gettext('Failed to backup Nginx config files: {0}'),
+  4006: () => $gettext('Failed to create hash info file: {0}'),
+  4007: () => $gettext('Failed to encrypt Nginx UI directory: {0}'),
+  4008: () => $gettext('Failed to encrypt Nginx directory: {0}'),
+  4009: () => $gettext('Failed to create zip archive: {0}'),
+  4011: () => $gettext('Failed to generate AES key: {0}'),
+  4012: () => $gettext('Failed to generate initialization vector: {0}'),
+  4013: () => $gettext('Failed to create backup file: {0}'),
+  4014: () => $gettext('Failed to cleanup temporary directory: {0}'),
+  4101: () => $gettext('Config path is empty'),
+  4102: () => $gettext('Failed to copy config file: {0}'),
+  4103: () => $gettext('Failed to copy database directory: {0}'),
+  4104: () => $gettext('Failed to copy database file: {0}'),
+  4105: () => $gettext('Failed to calculate hash: {0}'),
+  4106: () => $gettext('Nginx config directory is not set'),
+  4107: () => $gettext('Failed to copy Nginx config directory: {0}'),
+  4108: () => $gettext('Failed to read symlink: {0}'),
+  4201: () => $gettext('Failed to read file: {0}'),
+  4202: () => $gettext('Failed to encrypt file: {0}'),
+  4203: () => $gettext('Failed to write encrypted file: {0}'),
+  4204: () => $gettext('Failed to encrypt data: {0}'),
+  4205: () => $gettext('Failed to decrypt data: {0}'),
+  4206: () => $gettext('Invalid padding in decrypted data'),
+  4301: () => $gettext('Failed to create zip file: {0}'),
+  4302: () => $gettext('Failed to create zip entry: {0}'),
+  4303: () => $gettext('Failed to open source file: {0}'),
+  4304: () => $gettext('Failed to create zip header: {0}'),
+  4305: () => $gettext('Failed to copy file content: {0}'),
+  4306: () => $gettext('Failed to write to zip buffer: {0}'),
+  4501: () => $gettext('Failed to create restore directory: {0}'),
+  4505: () => $gettext('Failed to extract archive: {0}'),
+  4506: () => $gettext('Failed to decrypt Nginx UI directory: {0}'),
+  4507: () => $gettext('Failed to decrypt Nginx directory: {0}'),
+  4508: () => $gettext('Failed to verify hashes: {0}'),
+  4509: () => $gettext('Failed to restore Nginx configs: {0}'),
+  4510: () => $gettext('Failed to restore Nginx UI files: {0}'),
+  4511: () => $gettext('Backup file not found: {0}'),
+  4512: () => $gettext('Invalid security token format'),
+  4513: () => $gettext('Invalid AES key format: {0}'),
+  4514: () => $gettext('Invalid AES IV format: {0}'),
+  4601: () => $gettext('Failed to open zip file: {0}'),
+  4602: () => $gettext('Failed to create directory: {0}'),
+  4603: () => $gettext('Failed to create parent directory: {0}'),
+  4604: () => $gettext('Failed to create file: {0}'),
+  4605: () => $gettext('Failed to open zip entry: {0}'),
+  4606: () => $gettext('Failed to create symbolic link: {0}'),
+  4607: () => $gettext('Invalid file path: {0}'),
+  4608: () => $gettext('Failed to evaluate symbolic links: {0}'),
+  4701: () => $gettext('Failed to read encrypted file: {0}'),
+  4702: () => $gettext('Failed to decrypt file: {0}'),
+  4703: () => $gettext('Failed to write decrypted file: {0}'),
+  4801: () => $gettext('Failed to read hash info file: {0}'),
+  4802: () => $gettext('Failed to calculate Nginx UI hash: {0}'),
+  4803: () => $gettext('Failed to calculate Nginx hash: {0}'),
+  4804: () => $gettext('Hash verification failed: file integrity compromised'),
+}

+ 4 - 0
app/src/constants/errors/system.ts

@@ -0,0 +1,4 @@
+export default {
+  40301: () => $gettext('Nginx UI already installed'),
+  40302: () => $gettext('Installation is not allowed after 10 minutes of system startup'),
+}

文件差异内容过多而无法显示
+ 474 - 52
app/src/language/ar/app.po


文件差异内容过多而无法显示
+ 468 - 52
app/src/language/de_DE/app.po


文件差异内容过多而无法显示
+ 471 - 52
app/src/language/en/app.po


文件差异内容过多而无法显示
+ 468 - 52
app/src/language/es/app.po


文件差异内容过多而无法显示
+ 472 - 51
app/src/language/fr_FR/app.po


文件差异内容过多而无法显示
+ 464 - 53
app/src/language/ko_KR/app.po


文件差异内容过多而无法显示
+ 433 - 58
app/src/language/messages.pot


文件差异内容过多而无法显示
+ 444 - 97
app/src/language/ru_RU/app.po


文件差异内容过多而无法显示
+ 467 - 52
app/src/language/tr_TR/app.po


文件差异内容过多而无法显示
+ 464 - 53
app/src/language/vi_VN/app.po


文件差异内容过多而无法显示
+ 445 - 57
app/src/language/zh_CN/app.po


文件差异内容过多而无法显示
+ 475 - 52
app/src/language/zh_TW/app.po


+ 38 - 0
app/src/lib/http/client.ts

@@ -0,0 +1,38 @@
+import type { HttpConfig } from './types'
+import axios from 'axios'
+
+const instance = axios.create({
+  baseURL: import.meta.env.VITE_API_ROOT,
+  timeout: 50000,
+  headers: { 'Content-Type': 'application/json' },
+})
+
+const http = {
+  // eslint-disable-next-line ts/no-explicit-any
+  get<T = any>(url: string, config: HttpConfig = {}) {
+    // eslint-disable-next-line ts/no-explicit-any
+    return instance.get<any, T>(url, config)
+  },
+  // eslint-disable-next-line ts/no-explicit-any
+  post<T = any>(url: string, data: any = undefined, config: HttpConfig = {}) {
+    // eslint-disable-next-line ts/no-explicit-any
+    return instance.post<any, T>(url, data, config)
+  },
+  // eslint-disable-next-line ts/no-explicit-any
+  put<T = any>(url: string, data: any = undefined, config: HttpConfig = {}) {
+    // eslint-disable-next-line ts/no-explicit-any
+    return instance.put<any, T>(url, data, config)
+  },
+  // eslint-disable-next-line ts/no-explicit-any
+  delete<T = any>(url: string, config: HttpConfig = {}) {
+    // eslint-disable-next-line ts/no-explicit-any
+    return instance.delete<any, T>(url, config)
+  },
+  // eslint-disable-next-line ts/no-explicit-any
+  patch<T = any>(url: string, config: HttpConfig = {}) {
+    // eslint-disable-next-line ts/no-explicit-any
+    return instance.patch<any, T>(url, config)
+  },
+}
+
+export { http, instance }

+ 78 - 0
app/src/lib/http/error.ts

@@ -0,0 +1,78 @@
+import type { CosyError, CosyErrorRecord } from './types'
+import { message } from 'ant-design-vue'
+
+const errors: Record<string, CosyErrorRecord> = {}
+
+export function registerError(scope: string, record: CosyErrorRecord) {
+  errors[scope] = record
+}
+
+// Add new dedupe utility
+interface MessageDedupe {
+  error: (content: string, duration?: number) => void
+}
+
+export function useMessageDedupe(interval = 5000): MessageDedupe {
+  const lastMessages = new Map<string, number>()
+
+  return {
+    async error(content, duration = 5) {
+      const now = Date.now()
+      if (!lastMessages.has(content) || (now - (lastMessages.get(content) || 0)) > interval) {
+        lastMessages.set(content, now)
+        message.error(content, duration)
+      }
+    },
+  }
+}
+
+export function handleApiError(err: CosyError, dedupe: MessageDedupe) {
+  if (err?.scope) {
+    // check if already register
+    if (!errors[err.scope]) {
+      try {
+        // Dynamic import error files
+        import(`@/constants/errors/${err.scope}.ts`)
+          .then(error => {
+            registerError(err.scope!, error.default)
+            displayErrorMessage(err, dedupe)
+          })
+          .catch(() => {
+            dedupe.error($gettext(err?.message ?? 'Server error'))
+          })
+      }
+      catch {
+        dedupe.error($gettext(err?.message ?? 'Server error'))
+      }
+    }
+    else {
+      displayErrorMessage(err, dedupe)
+    }
+  }
+  else {
+    dedupe.error($gettext(err?.message ?? 'Server error'))
+  }
+}
+
+function displayErrorMessage(err: CosyError, dedupe: MessageDedupe) {
+  const msg = errors?.[err.scope ?? '']?.[err.code ?? '']
+
+  if (msg) {
+    // if err has params
+    if (err?.params && err.params.length > 0) {
+      let res = msg()
+
+      err.params.forEach((param, index) => {
+        res = res.replaceAll(`{${index}}`, param)
+      })
+
+      dedupe.error(res)
+    }
+    else {
+      dedupe.error(msg())
+    }
+  }
+  else {
+    dedupe.error($gettext(err?.message ?? 'Server error'))
+  }
+}

+ 14 - 184
app/src/lib/http/index.ts

@@ -1,189 +1,19 @@
-import type { AxiosRequestConfig } from 'axios'
-import use2FAModal from '@/components/TwoFA/use2FAModal'
-import { useNProgress } from '@/lib/nprogress/nprogress'
-import { useSettingsStore, useUserStore } from '@/pinia'
-import router from '@/routes'
-import { message } from 'ant-design-vue'
-import axios from 'axios'
-import JSEncrypt from 'jsencrypt'
-import { storeToRefs } from 'pinia'
-import 'nprogress/nprogress.css'
+import type { CosyError, CosyErrorRecord, HttpConfig } from './types'
+import { http } from './client'
+import { registerError, useMessageDedupe } from './error'
+import { setupInterceptors } from './interceptors'
 
-const user = useUserStore()
-const settings = useSettingsStore()
-const { token, secureSessionId } = storeToRefs(user)
-
-// server response
-export interface CosyError {
-  scope?: string
-  code: string
-  message: string
-  params?: string[]
-}
-
-// code, message translation
-export type CosyErrorRecord = Record<number, () => string>
-
-const errors: Record<string, CosyErrorRecord> = {}
-
-function registerError(scope: string, record: CosyErrorRecord) {
-  errors[scope] = record
-}
-
-const instance = axios.create({
-  baseURL: import.meta.env.VITE_API_ROOT,
-  timeout: 50000,
-  headers: { 'Content-Type': 'application/json' },
-})
-
-const http = {
-  get(url: string, config: AxiosRequestConfig = {}) {
-    // eslint-disable-next-line ts/no-explicit-any
-    return instance.get<any, any>(url, config)
-  },
-  // eslint-disable-next-line ts/no-explicit-any
-  post(url: string, data: any = undefined, config: AxiosRequestConfig = {}) {
-    // eslint-disable-next-line ts/no-explicit-any
-    return instance.post<any, any>(url, data, config)
-  },
-  // eslint-disable-next-line ts/no-explicit-any
-  put(url: string, data: any = undefined, config: AxiosRequestConfig = {}) {
-    // eslint-disable-next-line ts/no-explicit-any
-    return instance.put<any, any>(url, data, config)
-  },
-  delete(url: string, config: AxiosRequestConfig = {}) {
-    // eslint-disable-next-line ts/no-explicit-any
-    return instance.delete<any, any>(url, config)
-  },
-  patch(url: string, config: AxiosRequestConfig = {}) {
-    // eslint-disable-next-line ts/no-explicit-any
-    return instance.patch<any, any>(url, config)
-  },
-}
+// Initialize interceptors
+setupInterceptors()
 
+// Export everything needed from this module
 export default http
-
-const nprogress = useNProgress()
-
-// Add new dedupe utility at the top
-interface MessageDedupe {
-  error: (content: string, duration?: number) => void
+export type {
+  CosyError,
+  CosyErrorRecord,
+  HttpConfig,
 }
-
-function useMessageDedupe(interval = 5000): MessageDedupe {
-  const lastMessages = new Map<string, number>()
-
-  return {
-    async error(content, duration = 5) {
-      const now = Date.now()
-      if (!lastMessages.has(content) || (now - (lastMessages.get(content) || 0)) > interval) {
-        lastMessages.set(content, now)
-        message.error(content, duration)
-      }
-    },
-  }
+export {
+  registerError,
+  useMessageDedupe,
 }
-
-instance.interceptors.request.use(
-  async config => {
-    nprogress.start()
-    if (token.value) {
-      config.headers.Authorization = token.value
-    }
-
-    if (settings.environment.id) {
-      config.headers['X-Node-ID'] = settings.environment.id
-    }
-
-    if (secureSessionId.value) {
-      config.headers['X-Secure-Session-ID'] = secureSessionId.value
-    }
-
-    if (config.headers?.['Content-Type'] !== 'multipart/form-data;charset=UTF-8') {
-      config.headers['Content-Type'] = 'application/json'
-
-      if (config.crypto) {
-        const cryptoParams = await http.get('/crypto/public_key')
-        const { public_key } = await cryptoParams
-
-        // Encrypt data with RSA public key
-        const encrypt = new JSEncrypt()
-        encrypt.setPublicKey(public_key)
-
-        config.data = JSON.stringify({
-          encrypted_params: encrypt.encrypt(JSON.stringify(config.data)),
-        })
-      }
-    }
-    return config
-  },
-  err => {
-    return Promise.reject(err)
-  },
-)
-
-const dedupe = useMessageDedupe()
-
-instance.interceptors.response.use(
-  response => {
-    nprogress.done()
-    return Promise.resolve(response.data)
-  },
-  // eslint-disable-next-line sonarjs/cognitive-complexity
-  async error => {
-    nprogress.done()
-    const otpModal = use2FAModal()
-    switch (error.response.status) {
-      case 401:
-        secureSessionId.value = ''
-        await otpModal.open()
-        break
-      case 403:
-        user.logout()
-        await router.push('/login')
-        break
-    }
-
-    const err = error.response.data as CosyError
-
-    if (err?.scope) {
-      // check if already register
-      if (!errors[err.scope]) {
-        try {
-          const error = await import(`@/constants/errors/${err.scope}.ts`)
-
-          registerError(err.scope, error.default)
-        }
-        catch {
-          /* empty */
-        }
-      }
-
-      const msg = errors?.[err.scope]?.[err.code]
-
-      if (msg) {
-        // if err has params
-        if (err?.params && err.params.length > 0) {
-          let res = msg()
-
-          err.params.forEach((param, index) => {
-            res = res.replaceAll(`{${index}}`, param)
-          })
-
-          dedupe.error(res)
-        }
-        else {
-          dedupe.error(msg())
-        }
-      }
-      else {
-        dedupe.error($gettext(err?.message ?? 'Server error'))
-      }
-    }
-    else {
-      dedupe.error($gettext(err?.message ?? 'Server error'))
-    }
-
-    return Promise.reject(error.response.data)
-  },
-)

+ 160 - 0
app/src/lib/http/interceptors.ts

@@ -0,0 +1,160 @@
+import type { CosyError } from './types'
+import use2FAModal from '@/components/TwoFA/use2FAModal'
+import { useNProgress } from '@/lib/nprogress/nprogress'
+import { useSettingsStore, useUserStore } from '@/pinia'
+import router from '@/routes'
+import JSEncrypt from 'jsencrypt'
+import { storeToRefs } from 'pinia'
+import { http, instance } from './client'
+import { handleApiError, useMessageDedupe } from './error'
+
+// Setup stores and refs
+const user = useUserStore()
+const settings = useSettingsStore()
+const { token, secureSessionId } = storeToRefs(user)
+const nprogress = useNProgress()
+const dedupe = useMessageDedupe()
+
+// Helper function for encrypting JSON data
+// eslint-disable-next-line ts/no-explicit-any
+async function encryptJsonData(data: any): Promise<string> {
+  const cryptoParams = await http.get('/crypto/public_key')
+  const { public_key } = await cryptoParams
+
+  // Encrypt data with RSA public key
+  const encrypt = new JSEncrypt()
+  encrypt.setPublicKey(public_key)
+
+  return JSON.stringify({
+    encrypted_params: encrypt.encrypt(JSON.stringify(data)),
+  })
+}
+
+// Helper function for handling encrypted form data
+async function handleEncryptedFormData(formData: FormData): Promise<FormData> {
+  const cryptoParams = await http.get('/crypto/public_key')
+  const { public_key } = await cryptoParams
+
+  // Extract form parameters that are not files
+  // eslint-disable-next-line ts/no-explicit-any
+  const formParams: Record<string, any> = {}
+  const newFormData = new FormData()
+
+  // Copy all files to new FormData
+  for (const [key, value] of formData.entries()) {
+    // Check if value is a File or Blob
+    // eslint-disable-next-line ts/no-explicit-any
+    if (typeof value !== 'string' && ((value as any) instanceof File || (value as any) instanceof Blob)) {
+      newFormData.append(key, value)
+    }
+    else {
+      // Collect non-file fields to encrypt
+      formParams[key] = value
+    }
+  }
+
+  // Encrypt the form parameters
+  const encrypt = new JSEncrypt()
+  encrypt.setPublicKey(public_key)
+
+  // Add encrypted params to form data
+  const encryptedData = encrypt.encrypt(JSON.stringify(formParams))
+  if (encryptedData) {
+    newFormData.append('encrypted_params', encryptedData)
+  }
+
+  return newFormData
+}
+
+// Setup request interceptor
+export function setupRequestInterceptor() {
+  instance.interceptors.request.use(
+    async config => {
+      nprogress.start()
+      if (token.value) {
+        config.headers.Authorization = token.value
+      }
+
+      if (settings.environment.id) {
+        config.headers['X-Node-ID'] = settings.environment.id
+      }
+
+      if (secureSessionId.value) {
+        config.headers['X-Secure-Session-ID'] = secureSessionId.value
+      }
+
+      // Handle JSON encryption
+      if (config.headers?.['Content-Type'] !== 'multipart/form-data;charset=UTF-8') {
+        config.headers['Content-Type'] = 'application/json'
+
+        if (config.crypto) {
+          config.data = await encryptJsonData(config.data)
+        }
+      }
+      // Handle form data with encryption
+      else if (config.crypto && config.data instanceof FormData) {
+        config.data = await handleEncryptedFormData(config.data)
+      }
+
+      return config
+    },
+    err => {
+      return Promise.reject(err)
+    },
+  )
+}
+
+// Setup response interceptor
+export function setupResponseInterceptor() {
+  instance.interceptors.response.use(
+    response => {
+      nprogress.done()
+      // Check if full response is requested in config
+      if (response.config?.returnFullResponse) {
+        return Promise.resolve(response)
+      }
+      return Promise.resolve(response.data)
+    },
+
+    async error => {
+      nprogress.done()
+      const otpModal = use2FAModal()
+
+      // Handle authentication errors
+      if (error.response) {
+        switch (error.response.status) {
+          case 401:
+            secureSessionId.value = ''
+            await otpModal.open()
+            break
+          case 403:
+            user.logout()
+            await router.push('/login')
+            break
+        }
+      }
+
+      // Handle JSON error that comes back as Blob for blob request type
+      if (error.response?.data instanceof Blob && error.response.data.type === 'application/json') {
+        try {
+          const text = await error.response.data.text()
+          error.response.data = JSON.parse(text)
+        }
+        catch (e) {
+          // If parsing fails, we'll continue with the original error.response.data
+          console.error('Failed to parse blob error response as JSON', e)
+        }
+      }
+
+      const err = error.response?.data as CosyError
+      handleApiError(err, dedupe)
+
+      return Promise.reject(error.response?.data)
+    },
+  )
+}
+
+export function setupInterceptors() {
+  setupRequestInterceptor()
+  setupResponseInterceptor()
+}

+ 25 - 0
app/src/lib/http/types.ts

@@ -0,0 +1,25 @@
+import type { AxiosRequestConfig } from 'axios'
+
+// server response
+export interface CosyError {
+  scope?: string
+  code: string
+  message: string
+  params?: string[]
+}
+
+// code, message translation
+export type CosyErrorRecord = Record<number, () => string>
+
+export interface HttpConfig extends AxiosRequestConfig {
+  returnFullResponse?: boolean
+  crypto?: boolean
+}
+
+// Extend InternalAxiosRequestConfig type
+declare module 'axios' {
+  interface InternalAxiosRequestConfig {
+    returnFullResponse?: boolean
+    crypto?: boolean
+  }
+}

+ 1 - 0
app/src/lib/nprogress/nprogress.ts

@@ -1,5 +1,6 @@
 import _ from 'lodash'
 import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
 
 NProgress.configure({ showSpinner: false, trickleSpeed: 300 })
 

+ 37 - 310
app/src/routes/index.ts

@@ -1,25 +1,42 @@
 import type { RouteRecordRaw } from 'vue-router'
 import { useNProgress } from '@/lib/nprogress/nprogress'
-
-import { useSettingsStore, useUserStore } from '@/pinia'
-import {
-  BellOutlined,
-  CloudOutlined,
-  CodeOutlined,
-  DatabaseOutlined,
-  FileOutlined,
-  FileTextOutlined,
-  HomeOutlined,
-  InfoCircleOutlined,
-  SafetyCertificateOutlined,
-  SettingOutlined,
-  ShareAltOutlined,
-  UserOutlined,
-} from '@ant-design/icons-vue'
-
+import { useUserStore } from '@/pinia'
 import { createRouter, createWebHashHistory } from 'vue-router'
+import { authRoutes } from './modules/auth'
+
+import { certificatesRoutes } from './modules/certificates'
+import { configRoutes } from './modules/config'
+// Import module routes
+import { dashboardRoutes } from './modules/dashboard'
+import { environmentsRoutes } from './modules/environments'
+import { errorRoutes } from './modules/error'
+import { nginxLogRoutes } from './modules/nginx_log'
+import { notificationsRoutes } from './modules/notifications'
+import { preferenceRoutes } from './modules/preference'
+import { sitesRoutes } from './modules/sites'
+import { streamsRoutes } from './modules/streams'
+import { systemRoutes } from './modules/system'
+import { terminalRoutes } from './modules/terminal'
+import { userRoutes } from './modules/user'
 import 'nprogress/nprogress.css'
 
+// Combine child routes for the main layout
+const mainLayoutChildren: RouteRecordRaw[] = [
+  ...dashboardRoutes,
+  ...sitesRoutes,
+  ...streamsRoutes,
+  ...configRoutes,
+  ...certificatesRoutes,
+  ...terminalRoutes,
+  ...nginxLogRoutes,
+  ...environmentsRoutes,
+  ...notificationsRoutes,
+  ...userRoutes,
+  ...preferenceRoutes,
+  ...systemRoutes,
+]
+
+// Main routes configuration
 export const routes: RouteRecordRaw[] = [
   {
     path: '/',
@@ -29,300 +46,10 @@ export const routes: RouteRecordRaw[] = [
     meta: {
       name: () => $gettext('Home'),
     },
-    children: [
-      {
-        path: 'dashboard',
-        component: () => import('@/views/dashboard/DashBoard.vue'),
-        name: 'Dashboard',
-        meta: {
-          name: () => $gettext('Dashboard'),
-          icon: HomeOutlined,
-        },
-      },
-      {
-        path: 'sites',
-        name: 'Manage Sites',
-        component: () => import('@/layouts/BaseRouterView.vue'),
-        meta: {
-          name: () => $gettext('Manage Sites'),
-          icon: CloudOutlined,
-        },
-        redirect: '/sites/list',
-        children: [{
-          path: 'list',
-          name: 'Sites List',
-          component: () => import('@/views/site/site_list/SiteList.vue'),
-          meta: {
-            name: () => $gettext('Sites List'),
-          },
-        }, {
-          path: 'add',
-          name: 'Add Site',
-          component: () => import('@/views/site/site_add/SiteAdd.vue'),
-          meta: {
-            name: () => $gettext('Add Site'),
-            lastRouteName: 'Sites List',
-          },
-        }, {
-          path: 'categories',
-          name: 'Site Categories',
-          component: () => import('@/views/site/site_category/SiteCategory.vue'),
-          meta: {
-            name: () => $gettext('Site Categories'),
-          },
-        }, {
-          path: ':name',
-          name: 'Edit Site',
-          component: () => import('@/views/site/site_edit/SiteEdit.vue'),
-          meta: {
-            name: () => $gettext('Edit Site'),
-            hiddenInSidebar: true,
-            lastRouteName: 'Sites List',
-          },
-        }],
-      },
-      {
-        path: 'streams',
-        name: 'Manage Streams',
-        component: () => import('@/views/stream/StreamList.vue'),
-        meta: {
-          name: () => $gettext('Manage Streams'),
-          icon: ShareAltOutlined,
-        },
-      },
-      {
-        path: 'streams/:name',
-        name: 'Edit Stream',
-        component: () => import('@/views/stream/StreamEdit.vue'),
-        meta: {
-          name: () => $gettext('Edit Stream'),
-          hiddenInSidebar: true,
-          lastRouteName: 'Manage Streams',
-        },
-      },
-      {
-        path: 'config',
-        name: 'Manage Configs',
-        component: () => import('@/views/config/ConfigList.vue'),
-        meta: {
-          name: () => $gettext('Manage Configs'),
-          icon: FileOutlined,
-          hideChildren: true,
-        },
-      },
-      {
-        path: 'config/add',
-        name: 'Add Configuration',
-        component: () => import('@/views/config/ConfigEditor.vue'),
-        meta: {
-          name: () => $gettext('Add Configuration'),
-          hiddenInSidebar: true,
-          lastRouteName: 'Manage Configs',
-        },
-      },
-      {
-        path: 'config/:name+/edit',
-        name: 'Edit Configuration',
-        component: () => import('@/views/config/ConfigEditor.vue'),
-        meta: {
-          name: () => $gettext('Edit Configuration'),
-          hiddenInSidebar: true,
-          lastRouteName: 'Manage Configs',
-        },
-      },
-      {
-        path: 'certificates',
-        name: 'Certificates',
-        component: () => import('@/layouts/BaseRouterView.vue'),
-        redirect: '/certificates/list',
-        meta: {
-          name: () => $gettext('Certificates'),
-          icon: SafetyCertificateOutlined,
-        },
-        children: [
-          {
-            path: 'acme_users',
-            name: 'ACME User',
-            component: () => import('@/views/certificate/ACMEUser.vue'),
-            meta: {
-              name: () => $gettext('ACME User'),
-            },
-          },
-          {
-            path: 'list',
-            name: 'Certificates List',
-            component: () => import('@/views/certificate/CertificateList/Certificate.vue'),
-            meta: {
-              name: () => $gettext('Certificates List'),
-            },
-          },
-          {
-            path: ':id',
-            name: 'Modify Certificate',
-            component: () => import('@/views/certificate/CertificateEditor.vue'),
-            meta: {
-              name: () => $gettext('Modify Certificate'),
-              hiddenInSidebar: true,
-              lastRouteName: 'Certificates List',
-            },
-          },
-          {
-            path: 'import',
-            name: 'Import Certificate',
-            component: () => import('@/views/certificate/CertificateEditor.vue'),
-            meta: {
-              name: () => $gettext('Import Certificate'),
-              hiddenInSidebar: true,
-              lastRouteName: 'Certificates List',
-            },
-          },
-          {
-            path: 'dns_credential',
-            name: 'DNS Credentials',
-            component: () => import('@/views/certificate/DNSCredential.vue'),
-            meta: {
-              name: () => $gettext('DNS Credentials'),
-            },
-          },
-        ],
-      },
-      {
-        path: 'terminal',
-        name: 'Terminal',
-        component: () => import('@/views/terminal/Terminal.vue'),
-        meta: {
-          name: () => $gettext('Terminal'),
-          icon: CodeOutlined,
-        },
-      },
-      {
-        path: 'nginx_log',
-        name: 'Nginx Log',
-        meta: {
-          name: () => $gettext('Nginx Log'),
-          icon: FileTextOutlined,
-        },
-        children: [{
-          path: 'access',
-          name: 'Access Logs',
-          component: () => import('@/views/nginx_log/NginxLog.vue'),
-          meta: {
-            name: () => $gettext('Access Logs'),
-          },
-        }, {
-          path: 'error',
-          name: 'Error Logs',
-          component: () => import('@/views/nginx_log/NginxLog.vue'),
-          meta: {
-            name: () => $gettext('Error Logs'),
-          },
-        }, {
-          path: 'site',
-          name: 'Site Logs',
-          component: () => import('@/views/nginx_log/NginxLog.vue'),
-          meta: {
-            name: () => $gettext('Site Logs'),
-            hiddenInSidebar: true,
-          },
-        }],
-      },
-      {
-        path: 'environments',
-        name: 'Environments',
-        component: () => import('@/views/environment/Environment.vue'),
-        meta: {
-          name: () => $gettext('Environments'),
-          icon: DatabaseOutlined,
-          hiddenInSidebar: (): boolean => {
-            const settings = useSettingsStore()
-
-            return settings.is_remote
-          },
-        },
-      },
-      {
-        path: 'notifications',
-        name: 'Notifications',
-        component: () => import('@/views/notification/Notification.vue'),
-        meta: {
-          name: () => $gettext('Notifications'),
-          icon: BellOutlined,
-        },
-      },
-      {
-        path: 'user',
-        name: 'Manage Users',
-        component: () => import('@/views/user/User.vue'),
-        meta: {
-          name: () => $gettext('Manage Users'),
-          icon: UserOutlined,
-        },
-      },
-      {
-        path: 'preference',
-        name: 'Preference',
-        component: () => import('@/views/preference/Preference.vue'),
-        meta: {
-          name: () => $gettext('Preference'),
-          icon: SettingOutlined,
-        },
-      },
-      {
-        path: 'system',
-        name: 'System',
-        redirect: 'system/about',
-        meta: {
-          name: () => $gettext('System'),
-          icon: InfoCircleOutlined,
-        },
-        children: [{
-          path: 'self_check',
-          name: 'Self Check',
-          component: () => import('@/views/system/SelfCheck/SelfCheck.vue'),
-          meta: {
-            name: () => $gettext('Self Check'),
-          },
-        }, {
-          path: 'about',
-          name: 'About',
-          component: () => import('@/views/system/About.vue'),
-          meta: {
-            name: () => $gettext('About'),
-          },
-        }, {
-          path: 'upgrade',
-          name: 'Upgrade',
-          component: () => import('@/views/system/Upgrade.vue'),
-          meta: {
-            name: () => $gettext('Upgrade'),
-            hiddenInSidebar: (): boolean => {
-              const settings = useSettingsStore()
-
-              return settings.is_remote
-            },
-          },
-        }],
-      },
-    ],
-  },
-  {
-    path: '/install',
-    name: 'Install',
-    component: () => import('@/views/other/Install.vue'),
-    meta: { name: () => $gettext('Install'), noAuth: true },
-  },
-  {
-    path: '/login',
-    name: 'Login',
-    component: () => import('@/views/other/Login.vue'),
-    meta: { name: () => $gettext('Login'), noAuth: true },
-  },
-  {
-    path: '/:pathMatch(.*)*',
-    name: 'Not Found',
-    component: () => import('@/views/other/Error.vue'),
-    meta: { name: () => $gettext('Not Found'), noAuth: true, status_code: 404, error: () => $gettext('Not Found') },
+    children: mainLayoutChildren,
   },
+  ...authRoutes,
+  ...errorRoutes,
 ]
 
 const router = createRouter({

+ 16 - 0
app/src/routes/modules/auth.ts

@@ -0,0 +1,16 @@
+import type { RouteRecordRaw } from 'vue-router'
+
+export const authRoutes: RouteRecordRaw[] = [
+  {
+    path: '/install',
+    name: 'Install',
+    component: () => import('@/views/other/Install.vue'),
+    meta: { name: () => $gettext('Install'), noAuth: true },
+  },
+  {
+    path: '/login',
+    name: 'Login',
+    component: () => import('@/views/other/Login.vue'),
+    meta: { name: () => $gettext('Login'), noAuth: true },
+  },
+]

+ 61 - 0
app/src/routes/modules/certificates.ts

@@ -0,0 +1,61 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { SafetyCertificateOutlined } from '@ant-design/icons-vue'
+
+export const certificatesRoutes: RouteRecordRaw[] = [
+  {
+    path: 'certificates',
+    name: 'Certificates',
+    component: () => import('@/layouts/BaseRouterView.vue'),
+    redirect: '/certificates/list',
+    meta: {
+      name: () => $gettext('Certificates'),
+      icon: SafetyCertificateOutlined,
+    },
+    children: [
+      {
+        path: 'acme_users',
+        name: 'ACME User',
+        component: () => import('@/views/certificate/ACMEUser.vue'),
+        meta: {
+          name: () => $gettext('ACME User'),
+        },
+      },
+      {
+        path: 'list',
+        name: 'Certificates List',
+        component: () => import('@/views/certificate/CertificateList/Certificate.vue'),
+        meta: {
+          name: () => $gettext('Certificates List'),
+        },
+      },
+      {
+        path: ':id',
+        name: 'Modify Certificate',
+        component: () => import('@/views/certificate/CertificateEditor.vue'),
+        meta: {
+          name: () => $gettext('Modify Certificate'),
+          hiddenInSidebar: true,
+          lastRouteName: 'Certificates List',
+        },
+      },
+      {
+        path: 'import',
+        name: 'Import Certificate',
+        component: () => import('@/views/certificate/CertificateEditor.vue'),
+        meta: {
+          name: () => $gettext('Import Certificate'),
+          hiddenInSidebar: true,
+          lastRouteName: 'Certificates List',
+        },
+      },
+      {
+        path: 'dns_credential',
+        name: 'DNS Credentials',
+        component: () => import('@/views/certificate/DNSCredential.vue'),
+        meta: {
+          name: () => $gettext('DNS Credentials'),
+        },
+      },
+    ],
+  },
+]

+ 35 - 0
app/src/routes/modules/config.ts

@@ -0,0 +1,35 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { FileOutlined } from '@ant-design/icons-vue'
+
+export const configRoutes: RouteRecordRaw[] = [
+  {
+    path: 'config',
+    name: 'Manage Configs',
+    component: () => import('@/views/config/ConfigList.vue'),
+    meta: {
+      name: () => $gettext('Manage Configs'),
+      icon: FileOutlined,
+      hideChildren: true,
+    },
+  },
+  {
+    path: 'config/add',
+    name: 'Add Configuration',
+    component: () => import('@/views/config/ConfigEditor.vue'),
+    meta: {
+      name: () => $gettext('Add Configuration'),
+      hiddenInSidebar: true,
+      lastRouteName: 'Manage Configs',
+    },
+  },
+  {
+    path: 'config/:name+/edit',
+    name: 'Edit Configuration',
+    component: () => import('@/views/config/ConfigEditor.vue'),
+    meta: {
+      name: () => $gettext('Edit Configuration'),
+      hiddenInSidebar: true,
+      lastRouteName: 'Manage Configs',
+    },
+  },
+]

+ 14 - 0
app/src/routes/modules/dashboard.ts

@@ -0,0 +1,14 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { HomeOutlined } from '@ant-design/icons-vue'
+
+export const dashboardRoutes: RouteRecordRaw[] = [
+  {
+    path: 'dashboard',
+    component: () => import('@/views/dashboard/DashBoard.vue'),
+    name: 'Dashboard',
+    meta: {
+      name: () => $gettext('Dashboard'),
+      icon: HomeOutlined,
+    },
+  },
+]

+ 20 - 0
app/src/routes/modules/environments.ts

@@ -0,0 +1,20 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { useSettingsStore } from '@/pinia'
+import { DatabaseOutlined } from '@ant-design/icons-vue'
+
+export const environmentsRoutes: RouteRecordRaw[] = [
+  {
+    path: 'environments',
+    name: 'Environments',
+    component: () => import('@/views/environment/Environment.vue'),
+    meta: {
+      name: () => $gettext('Environments'),
+      icon: DatabaseOutlined,
+      hiddenInSidebar: (): boolean => {
+        const settings = useSettingsStore()
+
+        return settings.is_remote
+      },
+    },
+  },
+]

+ 10 - 0
app/src/routes/modules/error.ts

@@ -0,0 +1,10 @@
+import type { RouteRecordRaw } from 'vue-router'
+
+export const errorRoutes: RouteRecordRaw[] = [
+  {
+    path: '/:pathMatch(.*)*',
+    name: 'Not Found',
+    component: () => import('@/views/other/Error.vue'),
+    meta: { name: () => $gettext('Not Found'), noAuth: true, status_code: 404, error: () => $gettext('Not Found') },
+  },
+]

+ 36 - 0
app/src/routes/modules/nginx_log.ts

@@ -0,0 +1,36 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { FileTextOutlined } from '@ant-design/icons-vue'
+
+export const nginxLogRoutes: RouteRecordRaw[] = [
+  {
+    path: 'nginx_log',
+    name: 'Nginx Log',
+    meta: {
+      name: () => $gettext('Nginx Log'),
+      icon: FileTextOutlined,
+    },
+    children: [{
+      path: 'access',
+      name: 'Access Logs',
+      component: () => import('@/views/nginx_log/NginxLog.vue'),
+      meta: {
+        name: () => $gettext('Access Logs'),
+      },
+    }, {
+      path: 'error',
+      name: 'Error Logs',
+      component: () => import('@/views/nginx_log/NginxLog.vue'),
+      meta: {
+        name: () => $gettext('Error Logs'),
+      },
+    }, {
+      path: 'site',
+      name: 'Site Logs',
+      component: () => import('@/views/nginx_log/NginxLog.vue'),
+      meta: {
+        name: () => $gettext('Site Logs'),
+        hiddenInSidebar: true,
+      },
+    }],
+  },
+]

+ 14 - 0
app/src/routes/modules/notifications.ts

@@ -0,0 +1,14 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { BellOutlined } from '@ant-design/icons-vue'
+
+export const notificationsRoutes: RouteRecordRaw[] = [
+  {
+    path: 'notifications',
+    name: 'Notifications',
+    component: () => import('@/views/notification/Notification.vue'),
+    meta: {
+      name: () => $gettext('Notifications'),
+      icon: BellOutlined,
+    },
+  },
+]

+ 14 - 0
app/src/routes/modules/preference.ts

@@ -0,0 +1,14 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { SettingOutlined } from '@ant-design/icons-vue'
+
+export const preferenceRoutes: RouteRecordRaw[] = [
+  {
+    path: 'preference',
+    name: 'Preference',
+    component: () => import('@/views/preference/Preference.vue'),
+    meta: {
+      name: () => $gettext('Preference'),
+      icon: SettingOutlined,
+    },
+  },
+]

+ 47 - 0
app/src/routes/modules/sites.ts

@@ -0,0 +1,47 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { CloudOutlined } from '@ant-design/icons-vue'
+
+export const sitesRoutes: RouteRecordRaw[] = [
+  {
+    path: 'sites',
+    name: 'Manage Sites',
+    component: () => import('@/layouts/BaseRouterView.vue'),
+    meta: {
+      name: () => $gettext('Manage Sites'),
+      icon: CloudOutlined,
+    },
+    redirect: '/sites/list',
+    children: [{
+      path: 'list',
+      name: 'Sites List',
+      component: () => import('@/views/site/site_list/SiteList.vue'),
+      meta: {
+        name: () => $gettext('Sites List'),
+      },
+    }, {
+      path: 'add',
+      name: 'Add Site',
+      component: () => import('@/views/site/site_add/SiteAdd.vue'),
+      meta: {
+        name: () => $gettext('Add Site'),
+        lastRouteName: 'Sites List',
+      },
+    }, {
+      path: 'categories',
+      name: 'Site Categories',
+      component: () => import('@/views/site/site_category/SiteCategory.vue'),
+      meta: {
+        name: () => $gettext('Site Categories'),
+      },
+    }, {
+      path: ':name',
+      name: 'Edit Site',
+      component: () => import('@/views/site/site_edit/SiteEdit.vue'),
+      meta: {
+        name: () => $gettext('Edit Site'),
+        hiddenInSidebar: true,
+        lastRouteName: 'Sites List',
+      },
+    }],
+  },
+]

+ 24 - 0
app/src/routes/modules/streams.ts

@@ -0,0 +1,24 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { ShareAltOutlined } from '@ant-design/icons-vue'
+
+export const streamsRoutes: RouteRecordRaw[] = [
+  {
+    path: 'streams',
+    name: 'Manage Streams',
+    component: () => import('@/views/stream/StreamList.vue'),
+    meta: {
+      name: () => $gettext('Manage Streams'),
+      icon: ShareAltOutlined,
+    },
+  },
+  {
+    path: 'streams/:name',
+    name: 'Edit Stream',
+    component: () => import('@/views/stream/StreamEdit.vue'),
+    meta: {
+      name: () => $gettext('Edit Stream'),
+      hiddenInSidebar: true,
+      lastRouteName: 'Manage Streams',
+    },
+  },
+]

+ 49 - 0
app/src/routes/modules/system.ts

@@ -0,0 +1,49 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { useSettingsStore } from '@/pinia'
+import { InfoCircleOutlined } from '@ant-design/icons-vue'
+
+export const systemRoutes: RouteRecordRaw[] = [
+  {
+    path: 'system',
+    name: 'System',
+    redirect: 'system/about',
+    meta: {
+      name: () => $gettext('System'),
+      icon: InfoCircleOutlined,
+    },
+    children: [{
+      path: 'self_check',
+      name: 'Self Check',
+      component: () => import('@/views/system/SelfCheck/SelfCheck.vue'),
+      meta: {
+        name: () => $gettext('Self Check'),
+      },
+    }, {
+      path: 'about',
+      name: 'About',
+      component: () => import('@/views/system/About.vue'),
+      meta: {
+        name: () => $gettext('About'),
+      },
+    }, {
+      path: 'backup',
+      name: 'Backup',
+      component: () => import('@/views/system/Backup/index.vue'),
+      meta: {
+        name: () => $gettext('Backup'),
+      },
+    }, {
+      path: 'upgrade',
+      name: 'Upgrade',
+      component: () => import('@/views/system/Upgrade.vue'),
+      meta: {
+        name: () => $gettext('Upgrade'),
+        hiddenInSidebar: (): boolean => {
+          const settings = useSettingsStore()
+
+          return settings.is_remote
+        },
+      },
+    }],
+  },
+]

+ 14 - 0
app/src/routes/modules/terminal.ts

@@ -0,0 +1,14 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { CodeOutlined } from '@ant-design/icons-vue'
+
+export const terminalRoutes: RouteRecordRaw[] = [
+  {
+    path: 'terminal',
+    name: 'Terminal',
+    component: () => import('@/views/terminal/Terminal.vue'),
+    meta: {
+      name: () => $gettext('Terminal'),
+      icon: CodeOutlined,
+    },
+  },
+]

+ 14 - 0
app/src/routes/modules/user.ts

@@ -0,0 +1,14 @@
+import type { RouteRecordRaw } from 'vue-router'
+import { UserOutlined } from '@ant-design/icons-vue'
+
+export const userRoutes: RouteRecordRaw[] = [
+  {
+    path: 'user',
+    name: 'Manage Users',
+    component: () => import('@/views/user/User.vue'),
+    meta: {
+      name: () => $gettext('Manage Users'),
+      icon: UserOutlined,
+    },
+  },
+]

+ 1 - 1
app/src/version.json

@@ -1 +1 @@
-{"version":"2.0.0-rc.4","build_id":1,"total_build":386}
+{"version":"2.0.0-rc.4","build_id":8,"total_build":393}

+ 119 - 59
app/src/views/other/Install.vue

@@ -1,20 +1,46 @@
 <script setup lang="ts">
+import type { InstallLockResponse } from '@/api/install'
 import install from '@/api/install'
 import SetLanguage from '@/components/SetLanguage/SetLanguage.vue'
 import SwitchAppearance from '@/components/SwitchAppearance/SwitchAppearance.vue'
+import SystemRestoreContent from '@/components/SystemRestore/SystemRestoreContent.vue'
 import { DatabaseOutlined, LockOutlined, MailOutlined, UserOutlined } from '@ant-design/icons-vue'
 
-import { Form, message } from 'ant-design-vue'
-import { useRouter } from 'vue-router'
+import { Form, message, Tabs } from 'ant-design-vue'
+
+const TabPane = Tabs.TabPane
 
 const thisYear = new Date().getFullYear()
 const loading = ref(false)
+const installTimeout = ref(false)
+const activeTab = ref('1')
 
 const router = useRouter()
 
-install.get_lock().then(async (r: { lock: boolean }) => {
-  if (r.lock)
-    await router.push('/login')
+function init() {
+  install.get_lock().then(async (r: InstallLockResponse) => {
+    if (r.lock)
+      await router.push('/login')
+
+    if (r.timeout) {
+      installTimeout.value = true
+    }
+  })
+}
+
+onMounted(() => {
+  if (import.meta.env.DEV) {
+    const route = useRoute()
+    if (route.query.install !== 'false') {
+      init()
+    }
+    else {
+      installTimeout.value = route.query.timeout === 'true'
+    }
+  }
+  else {
+    init()
+  }
 })
 
 const modelRef = reactive({
@@ -66,11 +92,25 @@ function onSubmit() {
     install.install_nginx_ui(modelRef).then(async () => {
       message.success($gettext('Install successfully'))
       await router.push('/login')
+    }).catch(error => {
+      if (error && error.code === 40308) {
+        installTimeout.value = true
+      }
     }).finally(() => {
       loading.value = false
     })
   })
 }
+
+function handleRestoreSuccess(options: { restoreNginx: boolean, restoreNginxUI: boolean }): void {
+  message.success($gettext('System restored successfully.'))
+
+  // Only redirect to login page if Nginx UI was restored
+  if (options.restoreNginxUI) {
+    message.info($gettext('Please log in.'))
+    window.location.reload()
+  }
+}
 </script>
 
 <template>
@@ -81,60 +121,80 @@ function onSubmit() {
           <div class="project-title">
             <h1>Nginx UI</h1>
           </div>
-          <AForm id="components-form-install">
-            <AFormItem v-bind="validateInfos.email">
-              <AInput
-                v-model:value="modelRef.email"
-                :placeholder="$gettext('Email (*)')"
-              >
-                <template #prefix>
-                  <MailOutlined />
-                </template>
-              </AInput>
-            </AFormItem>
-            <AFormItem v-bind="validateInfos.username">
-              <AInput
-                v-model:value="modelRef.username"
-                :placeholder="$gettext('Username (*)')"
-              >
-                <template #prefix>
-                  <UserOutlined />
-                </template>
-              </AInput>
-            </AFormItem>
-            <AFormItem v-bind="validateInfos.password">
-              <AInputPassword
-                v-model:value="modelRef.password"
-                :placeholder="$gettext('Password (*)')"
-              >
-                <template #prefix>
-                  <LockOutlined />
-                </template>
-              </AInputPassword>
-            </AFormItem>
-            <AFormItem>
-              <AInput
-                v-bind="validateInfos.database"
-                v-model:value="modelRef.database"
-                :placeholder="$gettext('Database (Optional, default: database)')"
-              >
-                <template #prefix>
-                  <DatabaseOutlined />
-                </template>
-              </AInput>
-            </AFormItem>
-            <AFormItem>
-              <AButton
-                type="primary"
-                block
-                html-type="submit"
-                :loading="loading"
-                @click="onSubmit"
-              >
-                {{ $gettext('Install') }}
-              </AButton>
-            </AFormItem>
-          </AForm>
+          <AAlert
+            v-if="installTimeout"
+            type="warning"
+            :message="$gettext('Installation is not allowed after 10 minutes of system startup, please restart the Nginx UI.')"
+            show-icon
+            style="margin-bottom: 20px;"
+          />
+          <div v-else>
+            <Tabs v-model:active-key="activeTab">
+              <TabPane key="1" :tab="$gettext('New Installation')">
+                <AForm id="components-form-install">
+                  <AFormItem v-bind="validateInfos.email">
+                    <AInput
+                      v-model:value="modelRef.email"
+                      :placeholder="$gettext('Email (*)')"
+                    >
+                      <template #prefix>
+                        <MailOutlined />
+                      </template>
+                    </AInput>
+                  </AFormItem>
+                  <AFormItem v-bind="validateInfos.username">
+                    <AInput
+                      v-model:value="modelRef.username"
+                      :placeholder="$gettext('Username (*)')"
+                    >
+                      <template #prefix>
+                        <UserOutlined />
+                      </template>
+                    </AInput>
+                  </AFormItem>
+                  <AFormItem v-bind="validateInfos.password">
+                    <AInputPassword
+                      v-model:value="modelRef.password"
+                      :placeholder="$gettext('Password (*)')"
+                    >
+                      <template #prefix>
+                        <LockOutlined />
+                      </template>
+                    </AInputPassword>
+                  </AFormItem>
+                  <AFormItem>
+                    <AInput
+                      v-bind="validateInfos.database"
+                      v-model:value="modelRef.database"
+                      :placeholder="$gettext('Database (Optional, default: database)')"
+                    >
+                      <template #prefix>
+                        <DatabaseOutlined />
+                      </template>
+                    </AInput>
+                  </AFormItem>
+                  <AFormItem>
+                    <AButton
+                      type="primary"
+                      block
+                      html-type="submit"
+                      :loading="loading"
+                      :disabled="installTimeout"
+                      @click="onSubmit"
+                    >
+                      {{ $gettext('Install') }}
+                    </AButton>
+                  </AFormItem>
+                </AForm>
+              </TabPane>
+              <TabPane key="2" :tab="$gettext('Restore from Backup')">
+                <SystemRestoreContent
+                  :show-title="false"
+                  @restore-success="handleRestoreSuccess"
+                />
+              </TabPane>
+            </Tabs>
+          </div>
           <div class="footer">
             <p>Copyright © 2021 - {{ thisYear }} Nginx UI</p>
             Language

+ 245 - 0
app/src/views/system/Backup/BackupCreator.vue

@@ -0,0 +1,245 @@
+<script setup lang="tsx">
+import backup from '@/api/backup'
+import { CheckOutlined, CopyOutlined, InfoCircleFilled, WarningOutlined } from '@ant-design/icons-vue'
+import { UseClipboard } from '@vueuse/components'
+import { message } from 'ant-design-vue'
+import { ref } from 'vue'
+
+const isCreatingBackup = ref(false)
+const showSecurityModal = ref(false)
+const currentSecurityToken = ref('')
+const isCopied = ref(false)
+
+async function handleCreateBackup() {
+  try {
+    isCreatingBackup.value = true
+    const response = await backup.createBackup()
+
+    // Extract filename from Content-Disposition header if available
+    const contentDisposition = response.headers['content-disposition']
+    let filename = 'nginx-ui-backup.zip'
+    if (contentDisposition) {
+      const filenameMatch = contentDisposition.match(/filename=(.+)/)
+      if (filenameMatch && filenameMatch[1]) {
+        filename = filenameMatch[1].replace(/"/g, '')
+      }
+    }
+
+    // Extract security token from header
+    const securityToken = response.headers['x-backup-security']
+
+    // Create download link
+    const url = window.URL.createObjectURL(new Blob([response.data]))
+    const link = document.createElement('a')
+    link.href = url
+    link.setAttribute('download', filename)
+    document.body.appendChild(link)
+    link.click()
+    document.body.removeChild(link)
+
+    // Show security token to user
+    if (securityToken) {
+      message.success($gettext('Backup has been downloaded successfully'))
+
+      // Show the security token modal
+      currentSecurityToken.value = securityToken
+      showSecurityModal.value = true
+    }
+  }
+  catch (error) {
+    console.error('Backup download failed:', error)
+  }
+  finally {
+    isCreatingBackup.value = false
+  }
+}
+
+function handleCloseModal() {
+  showSecurityModal.value = false
+}
+
+function handleCopy(copy) {
+  copy()
+  isCopied.value = true
+  setTimeout(() => {
+    isCopied.value = false
+  }, 2000)
+}
+</script>
+
+<template>
+  <ACard :title="$gettext('System Backup')" :bordered="false">
+    <AAlert
+      show-icon
+      type="info"
+      :message="$gettext('Create system backups including Nginx configuration and Nginx UI settings. Backup files will be automatically downloaded to your computer.')"
+      class="mb-4"
+    />
+
+    <div class="flex justify-between">
+      <ASpace>
+        <AButton
+          type="primary"
+          :loading="isCreatingBackup"
+          @click="handleCreateBackup"
+        >
+          {{ $gettext('Create Backup') }}
+        </AButton>
+      </ASpace>
+    </div>
+
+    <!-- Security Token Modal Component -->
+    <AModal
+      v-model:open="showSecurityModal"
+      :title="$gettext('Security Token Information')"
+      :mask-closable="false"
+      :centered="true"
+      class="backup-token-modal"
+      width="550"
+      @ok="handleCloseModal"
+    >
+      <template #icon>
+        <InfoCircleFilled style="color: #1677ff; font-size: 22px" />
+      </template>
+
+      <div class="security-token-info py-2">
+        <p class="mb-4">
+          {{ $gettext('Please save this security token, you will need it for restoration:') }}
+        </p>
+
+        <div class="token-display mb-5">
+          <div class="token-container p-4 bg-gray-50 border border-gray-200 rounded-md mb-2">
+            <div class="token-text font-mono select-all break-all leading-relaxed">
+              {{ currentSecurityToken }}
+            </div>
+          </div>
+
+          <div class="flex justify-end mt-3">
+            <UseClipboard v-slot="{ copy }" :source="currentSecurityToken">
+              <AButton
+                type="primary"
+                :style="{ backgroundColor: isCopied ? '#52c41a' : undefined }"
+                @click="handleCopy(copy)"
+              >
+                <template #icon>
+                  <CheckOutlined v-if="isCopied" />
+                  <CopyOutlined v-else />
+                </template>
+                {{ isCopied ? $gettext('Copied!') : $gettext('Copy') }}
+              </AButton>
+            </UseClipboard>
+          </div>
+        </div>
+
+        <div class="warning-box flex items-start bg-red-50 border border-red-200 p-4 rounded-md">
+          <WarningOutlined class="text-red-500 mt-0.5 mr-2 flex-shrink-0" />
+          <div>
+            <p class="text-red-600 font-medium mb-1">
+              {{ $gettext('Warning') }}
+            </p>
+            <p class="text-red-600 mb-0 text-sm leading-relaxed">
+              {{ $gettext('This token will only be shown once and cannot be retrieved later. Please make sure to save it in a secure location.') }}
+            </p>
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <AButton type="primary" @click="handleCloseModal">
+          {{ $gettext('OK') }}
+        </AButton>
+      </template>
+    </AModal>
+  </ACard>
+</template>
+
+<style scoped>
+.security-token-info {
+  text-align: left;
+}
+.token-container {
+  word-break: break-all;
+  box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+.token-text {
+  line-height: 1.6;
+}
+
+/* Dark mode optimization */
+:deep(.backup-token-modal) {
+  /* Modal background */
+  .ant-modal-content {
+    background-color: #1f1f1f;
+  }
+
+  /* Modal title */
+  .ant-modal-header {
+    background-color: #1f1f1f;
+    border-bottom: 1px solid #303030;
+  }
+
+  .ant-modal-title {
+    color: #e6e6e6;
+  }
+
+  /* Modal content */
+  .ant-modal-body {
+    color: #e6e6e6;
+  }
+
+  /* Modal footer */
+  .ant-modal-footer {
+    border-top: 1px solid #303030;
+    background-color: #1f1f1f;
+  }
+
+  /* Close button */
+  .ant-modal-close-x {
+    color: #e6e6e6;
+  }
+}
+
+/* Token container dark mode styles */
+.dark {
+  .token-container {
+    background-color: #262626 !important;
+    border-color: #303030 !important;
+    box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.2);
+  }
+
+  .token-text {
+    color: #d9d9d9;
+  }
+
+  /* Warning box dark mode */
+  .warning-box {
+    background-color: rgba(255, 77, 79, 0.1);
+    border-color: rgba(255, 77, 79, 0.3);
+
+    p {
+      color: #ff7875;
+    }
+  }
+}
+
+/* Dark mode support via media query */
+@media (prefers-color-scheme: dark) {
+  .token-container {
+    background-color: #262626 !important;
+    border-color: #303030 !important;
+  }
+
+  .token-text {
+    color: #d9d9d9;
+  }
+
+  .warning-box {
+    background-color: rgba(255, 77, 79, 0.1);
+    border-color: rgba(255, 77, 79, 0.3);
+
+    p {
+      color: #ff7875;
+    }
+  }
+}
+</style>

+ 18 - 0
app/src/views/system/Backup/SystemRestore.vue

@@ -0,0 +1,18 @@
+<script setup lang="ts">
+import SystemRestoreContent from '@/components/SystemRestore/SystemRestoreContent.vue'
+import { message } from 'ant-design-vue'
+
+function handleRestoreSuccess(options: { restoreNginx: boolean, restoreNginxUI: boolean }): void {
+  message.success($gettext('System restored successfully.'))
+
+  // Only redirect to login page if Nginx UI was restored
+  if (options.restoreNginxUI) {
+    message.info($gettext('Please log in.'))
+    window.location.reload()
+  }
+}
+</script>
+
+<template>
+  <SystemRestoreContent :show-title="true" @restore-success="handleRestoreSuccess" />
+</template>

+ 11 - 0
app/src/views/system/Backup/index.vue

@@ -0,0 +1,11 @@
+<script setup lang="ts">
+import BackupCreator from './BackupCreator.vue'
+import SystemRestore from './SystemRestore.vue'
+</script>
+
+<template>
+  <div>
+    <BackupCreator />
+    <SystemRestore class="mt-4" />
+  </div>
+</template>

+ 66 - 70
go.mod

@@ -16,7 +16,7 @@ require (
 	github.com/gin-gonic/gin v1.10.0
 	github.com/go-acme/lego/v4 v4.22.2
 	github.com/go-co-op/gocron/v2 v2.16.1
-	github.com/go-playground/validator/v10 v10.25.0
+	github.com/go-playground/validator/v10 v10.26.0
 	github.com/go-resty/resty/v2 v2.16.5
 	github.com/go-webauthn/webauthn v0.12.2
 	github.com/golang-jwt/jwt/v5 v5.2.2
@@ -34,12 +34,12 @@ require (
 	github.com/shirou/gopsutil/v4 v4.25.2
 	github.com/spf13/cast v1.7.1
 	github.com/stretchr/testify v1.10.0
-	github.com/tufanbarisyildirim/gonginx v0.0.0-20250120210832-12a9c7ae0c8a
-	github.com/uozi-tech/cosy v1.16.0
+	github.com/tufanbarisyildirim/gonginx v0.0.0-20250225174229-c03497ddaef6
+	github.com/uozi-tech/cosy v1.17.0
 	github.com/uozi-tech/cosy-driver-sqlite v0.2.1
 	github.com/urfave/cli/v3 v3.0.0-beta1
 	golang.org/x/crypto v0.36.0
-	golang.org/x/net v0.37.0
+	golang.org/x/net v0.38.0
 	gopkg.in/ini.v1 v1.67.0
 	gorm.io/driver/sqlite v1.5.7
 	gorm.io/gen v0.3.26
@@ -49,13 +49,13 @@ require (
 
 require (
 	aead.dev/minisign v0.3.0 // indirect
-	cloud.google.com/go/auth v0.14.1 // indirect
-	cloud.google.com/go/auth/oauth2adapt v0.2.7 // indirect
+	cloud.google.com/go/auth v0.15.0 // indirect
+	cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
 	cloud.google.com/go/compute/metadata v0.6.0 // indirect
 	filippo.io/edwards25519 v1.1.0 // indirect
 	github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 // indirect
 	github.com/Azure/azure-sdk-for-go v68.0.0+incompatible // indirect
-	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect
+	github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
 	github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
@@ -70,34 +70,34 @@ require (
 	github.com/Azure/go-autorest/autorest/to v0.4.1 // indirect
 	github.com/Azure/go-autorest/logger v0.2.2 // indirect
 	github.com/Azure/go-autorest/tracing v0.6.1 // indirect
-	github.com/AzureAD/microsoft-authentication-library-for-go v1.4.0 // indirect
+	github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect
 	github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
 	github.com/StackExchange/wmi v1.2.1 // indirect
 	github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
-	github.com/aliyun/alibaba-cloud-sdk-go v1.63.88 // indirect
-	github.com/aws/aws-sdk-go-v2 v1.36.2 // indirect
-	github.com/aws/aws-sdk-go-v2/config v1.29.7 // indirect
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.60 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 // indirect
+	github.com/aliyun/alibaba-cloud-sdk-go v1.63.103 // indirect
+	github.com/aws/aws-sdk-go-v2 v1.36.3 // indirect
+	github.com/aws/aws-sdk-go-v2/config v1.29.12 // indirect
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.65 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14 // indirect
-	github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.16 // indirect
-	github.com/aws/aws-sdk-go-v2/service/route53 v1.48.8 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.24.16 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sts v1.33.15 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
+	github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1 // indirect
+	github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect
 	github.com/aws/smithy-go v1.22.3 // indirect
 	github.com/benbjohnson/clock v1.3.5 // indirect
 	github.com/boombuler/barcode v1.0.2 // indirect
 	github.com/bsm/redislock v0.9.4 // indirect
-	github.com/bytedance/sonic v1.13.1 // indirect
+	github.com/bytedance/sonic v1.13.2 // indirect
 	github.com/bytedance/sonic/loader v0.2.4 // indirect
 	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
 	github.com/cespare/xxhash/v2 v2.3.0 // indirect
-	github.com/civo/civogo v0.3.94 // indirect
+	github.com/civo/civogo v0.3.96 // indirect
 	github.com/cloudflare/cloudflare-go v0.115.0 // indirect
 	github.com/cloudwego/base64x v0.1.5 // indirect
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
@@ -105,7 +105,7 @@ require (
 	github.com/dimchansky/utfbom v1.1.1 // indirect
 	github.com/dnsimple/dnsimple-go v1.7.0 // indirect
 	github.com/ebitengine/purego v0.8.2 // indirect
-	github.com/exoscale/egoscale/v3 v3.1.9 // indirect
+	github.com/exoscale/egoscale/v3 v3.1.13 // indirect
 	github.com/fatih/color v1.18.0 // indirect
 	github.com/fatih/structs v1.1.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -115,25 +115,25 @@ require (
 	github.com/ghodss/yaml v1.0.0 // indirect
 	github.com/gin-contrib/sse v1.0.0 // indirect
 	github.com/go-errors/errors v1.5.1 // indirect
-	github.com/go-gormigrate/gormigrate/v2 v2.1.3 // indirect
-	github.com/go-jose/go-jose/v4 v4.0.4 // indirect
+	github.com/go-gormigrate/gormigrate/v2 v2.1.4 // indirect
+	github.com/go-jose/go-jose/v4 v4.0.5 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-logr/stdr v1.2.2 // indirect
 	github.com/go-ole/go-ole v1.3.0 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
-	github.com/go-sql-driver/mysql v1.9.0 // indirect
+	github.com/go-sql-driver/mysql v1.9.1 // indirect
 	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
 	github.com/go-webauthn/x v0.1.19 // indirect
 	github.com/goccy/go-json v0.10.5 // indirect
 	github.com/gofrs/flock v0.12.1 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
-	github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
+	github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/go-tpm v0.9.3 // indirect
 	github.com/google/gofuzz v1.2.0 // indirect
 	github.com/google/s2a-go v0.1.9 // indirect
-	github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect
+	github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
 	github.com/googleapis/gax-go/v2 v2.14.1 // indirect
 	github.com/gophercloud/gophercloud v1.14.1 // indirect
 	github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 // indirect
@@ -143,8 +143,7 @@ require (
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
 	github.com/hashicorp/go-uuid v1.0.3 // indirect
-	github.com/hashicorp/hcl v1.0.0 // indirect
-	github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.136 // indirect
+	github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.142 // indirect
 	github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
 	github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
 	github.com/itchyny/timefmt-go v0.1.6 // indirect
@@ -167,15 +166,14 @@ require (
 	github.com/labbsr0x/bindman-dns-webhook v1.0.2 // indirect
 	github.com/labbsr0x/goh v1.0.1 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
-	github.com/linode/linodego v1.47.0 // indirect
+	github.com/linode/linodego v1.48.1 // indirect
 	github.com/liquidweb/liquidweb-cli v0.7.0 // indirect
 	github.com/liquidweb/liquidweb-go v1.6.4 // indirect
-	github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
-	github.com/magiconair/properties v1.8.9 // indirect
+	github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
 	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
 	github.com/mattn/go-sqlite3 v1.14.24 // indirect
-	github.com/miekg/dns v1.1.63 // indirect
+	github.com/miekg/dns v1.1.64 // indirect
 	github.com/mimuret/golang-iij-dpf v0.9.1 // indirect
 	github.com/mitchellh/go-homedir v1.1.0 // indirect
 	github.com/mitchellh/mapstructure v1.5.0 // indirect
@@ -183,8 +181,8 @@ require (
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
 	github.com/nrdcg/auroradns v1.1.0 // indirect
-	github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect
-	github.com/nrdcg/desec v0.10.0 // indirect
+	github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea // indirect
+	github.com/nrdcg/desec v0.11.0 // indirect
 	github.com/nrdcg/dnspod-go v0.4.0 // indirect
 	github.com/nrdcg/freemyip v0.3.0 // indirect
 	github.com/nrdcg/goacmedns v0.2.0 // indirect
@@ -196,7 +194,7 @@ require (
 	github.com/nxadm/tail v1.4.11 // indirect
 	github.com/nzdjb/go-metaname v1.0.0 // indirect
 	github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect
-	github.com/oracle/oci-go-sdk/v65 v65.83.2 // indirect
+	github.com/oracle/oci-go-sdk/v65 v65.88.0 // indirect
 	github.com/ovh/go-ovh v1.7.0 // indirect
 	github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
 	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
@@ -211,9 +209,8 @@ require (
 	github.com/sacloud/go-http v0.1.9 // indirect
 	github.com/sacloud/iaas-api-go v1.14.0 // indirect
 	github.com/sacloud/packages-go v0.0.11 // indirect
-	github.com/sagikazarmark/locafero v0.7.0 // indirect
-	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
-	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32 // indirect
+	github.com/sagikazarmark/locafero v0.9.0 // indirect
+	github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 // indirect
 	github.com/selectel/domains-go v1.1.0 // indirect
 	github.com/selectel/go-selvpcclient/v3 v3.2.1 // indirect
 	github.com/shopspring/decimal v1.4.0 // indirect
@@ -224,15 +221,16 @@ require (
 	github.com/sony/gobreaker v1.0.0 // indirect
 	github.com/sony/sonyflake v1.2.0 // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
-	github.com/spf13/afero v1.12.0 // indirect
+	github.com/spf13/afero v1.14.0 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
-	github.com/spf13/viper v1.19.0 // indirect
+	github.com/spf13/viper v1.20.1 // indirect
+	github.com/stretchr/objx v0.5.2 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
-	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1101 // indirect
-	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1101 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1133 // indirect
+	github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128 // indirect
 	github.com/tjfoc/gmsm v1.4.1 // indirect
-	github.com/tklauser/go-sysconf v0.3.14 // indirect
-	github.com/tklauser/numcpus v0.9.0 // indirect
+	github.com/tklauser/go-sysconf v0.3.15 // indirect
+	github.com/tklauser/numcpus v0.10.0 // indirect
 	github.com/transip/gotransip/v6 v6.26.0 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.12 // indirect
@@ -240,39 +238,37 @@ require (
 	github.com/uozi-tech/cosy-driver-mysql v0.2.2 // indirect
 	github.com/uozi-tech/cosy-driver-postgres v0.2.1 // indirect
 	github.com/vinyldns/go-vinyldns v0.9.16 // indirect
-	github.com/volcengine/volc-sdk-golang v1.0.195 // indirect
-	github.com/vultr/govultr/v3 v3.14.1 // indirect
+	github.com/volcengine/volc-sdk-golang v1.0.201 // indirect
+	github.com/vultr/govultr/v3 v3.18.0 // indirect
 	github.com/x448/float16 v0.8.4 // indirect
-	github.com/yandex-cloud/go-genproto v0.0.0-20250217111757-ccaea642a16c // indirect
-	github.com/yandex-cloud/go-sdk v0.0.0-20250210144447-399a857b9c4e // indirect
+	github.com/yandex-cloud/go-genproto v0.0.0-20250325081613-cd85d9003939 // indirect
+	github.com/yandex-cloud/go-sdk v0.0.0-20250325134853-dcb34ef70818 // indirect
 	github.com/yusufpapurcu/wmi v1.2.4 // indirect
-	go.mongodb.org/mongo-driver v1.17.2 // indirect
+	go.mongodb.org/mongo-driver v1.17.3 // indirect
 	go.opentelemetry.io/auto/sdk v1.1.0 // indirect
-	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
-	go.opentelemetry.io/otel v1.34.0 // indirect
-	go.opentelemetry.io/otel/metric v1.34.0 // indirect
-	go.opentelemetry.io/otel/trace v1.34.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
+	go.opentelemetry.io/otel v1.35.0 // indirect
+	go.opentelemetry.io/otel/metric v1.35.0 // indirect
+	go.opentelemetry.io/otel/trace v1.35.0 // indirect
 	go.uber.org/atomic v1.11.0 // indirect
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/ratelimit v0.3.1 // indirect
 	go.uber.org/zap v1.27.0 // indirect
 	golang.org/x/arch v0.15.0 // indirect
-	golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
 	golang.org/x/mod v0.24.0 // indirect
-	golang.org/x/oauth2 v0.26.0 // indirect
+	golang.org/x/oauth2 v0.28.0 // indirect
 	golang.org/x/sync v0.12.0 // indirect
 	golang.org/x/sys v0.31.0 // indirect
 	golang.org/x/text v0.23.0 // indirect
-	golang.org/x/time v0.10.0 // indirect
+	golang.org/x/time v0.11.0 // indirect
 	golang.org/x/tools v0.31.0 // indirect
-	google.golang.org/api v0.221.0 // indirect
-	google.golang.org/genproto v0.0.0-20250218202821-56aae31c358a // indirect
-	google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
-	google.golang.org/grpc v1.70.0 // indirect
-	google.golang.org/protobuf v1.36.5 // indirect
+	google.golang.org/api v0.228.0 // indirect
+	google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
+	google.golang.org/grpc v1.71.0 // indirect
+	google.golang.org/protobuf v1.36.6 // indirect
 	gopkg.in/fsnotify.v1 v1.4.7 // indirect
-	gopkg.in/guregu/null.v4 v4.0.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
 	gopkg.in/ns1/ns1-go.v2 v2.13.0 // indirect
@@ -283,11 +279,11 @@ require (
 	gorm.io/driver/mysql v1.5.7 // indirect
 	gorm.io/driver/postgres v1.5.9 // indirect
 	gorm.io/hints v1.1.2 // indirect
-	k8s.io/api v0.32.2 // indirect
-	k8s.io/apimachinery v0.32.2 // indirect
+	k8s.io/api v0.32.3 // indirect
+	k8s.io/apimachinery v0.32.3 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
-	k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect
+	k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e // indirect
 	sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
-	sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
+	sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
 	sigs.k8s.io/yaml v1.4.0 // indirect
 )

+ 140 - 203
go.sum

@@ -100,10 +100,10 @@ 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.14.1 h1:AwoJbzUdxA/whv1qj3TLKwh3XX5sikny2fc40wUl+h0=
-cloud.google.com/go/auth v0.14.1/go.mod h1:4JHUxlGXisL0AW8kXPtUF6ztuOksyfUQNFjfsOCXkPM=
-cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M=
-cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
+cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps=
+cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8=
+cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
+cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
 cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0=
 cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8=
 cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8=
@@ -612,8 +612,8 @@ github.com/AdamSLevy/jsonrpc2/v14 v14.1.0 h1:Dy3M9aegiI7d7PF1LUdjbVigJReo+QOceYs
 github.com/AdamSLevy/jsonrpc2/v14 v14.1.0/go.mod h1:ZakZtbCXxCz82NJvq7MoREtiQesnDfrtF6RFUGzQfLo=
 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU=
 github.com/Azure/azure-sdk-for-go v68.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ=
-github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1 h1:DSDNVxqkoXJiko6x8a90zidoYqnYYa6c1MTzDKzKkTo=
+github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.1/go.mod h1:zGqV2R4Cr/k8Uye5w+dgQ06WJtEcbQG/8J7BB6hnCr4=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
 github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
@@ -660,12 +660,10 @@ github.com/Azure/go-autorest/tracing v0.6.1 h1:YUMSrC/CeD1ZnnXcNYU4a/fzsO35u2Fsf
 github.com/Azure/go-autorest/tracing v0.6.1/go.mod h1:/3EgjbsjraOqiicERAeu3m7/z0x1TzjQGAwDrJrXGkc=
 github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
 github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.4.0 h1:MUkXAnvvDHgvPItl0nBj0hgk0f7hnnQbGm0h0+YxbN4=
-github.com/AzureAD/microsoft-authentication-library-for-go v1.4.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
+github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
-github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
-github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
 github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
 github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -698,8 +696,8 @@ 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.88 h1:87jNTxliGqU2yB3H09xCd4U3cZCmR4AkOMqWgaluo5Q=
-github.com/aliyun/alibaba-cloud-sdk-go v1.63.88/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
+github.com/aliyun/alibaba-cloud-sdk-go v1.63.103 h1:kZsvZo6waUg5313S6VkoPx8QyyeoUfMgF/KgxpiEfCw=
+github.com/aliyun/alibaba-cloud-sdk-go v1.63.103/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
 github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
 github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
 github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
@@ -714,35 +712,35 @@ github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
 github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
 github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
 github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
-github.com/aws/aws-sdk-go-v2 v1.36.2 h1:Ub6I4lq/71+tPb/atswvToaLGVMxKZvjYDVOWEExOcU=
-github.com/aws/aws-sdk-go-v2 v1.36.2/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
-github.com/aws/aws-sdk-go-v2/config v1.29.7 h1:71nqi6gUbAUiEQkypHQcNVSFJVUFANpSeUNShiwWX2M=
-github.com/aws/aws-sdk-go-v2/config v1.29.7/go.mod h1:yqJQ3nh2HWw/uxd56bicyvmDW4KSc+4wN6lL8pYjynU=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.60 h1:1dq+ELaT5ogfmqtV1eocq8SpOK1NRsuUfmhQtD/XAh4=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.60/go.mod h1:HDes+fn/xo9VeszXqjBVkxOo/aUy8Mc6QqKvZk32GlE=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29 h1:JO8pydejFKmGcUNiiwt75dzLHRWthkwApIvPoyUtXEg=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.29/go.mod h1:adxZ9i9DRmB8zAT0pO0yGnsmu0geomp5a3uq5XpgOJ8=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33 h1:knLyPMw3r3JsU8MFHWctE4/e2qWbPaxDYLlohPvnY8c=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.33/go.mod h1:EBp2HQ3f+XCB+5J+IoEbGhoV7CpJbnrsd4asNXmTL0A=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33 h1:K0+Ne08zqti8J9jwENxZ5NoUyBnaFDTu3apwQJWrwwA=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.33/go.mod h1:K97stwwzaWzmqxO8yLGHhClbVW1tC6VT1pDLk1pGrq4=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/config v1.29.12 h1:Y/2a+jLPrPbHpFkpAAYkVEtJmxORlXoo5k2g1fa2sUo=
+github.com/aws/aws-sdk-go-v2/config v1.29.12/go.mod h1:xse1YTjmORlb/6fhkWi8qJh3cvZi4JoVNhc+NbJt4kI=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.65 h1:q+nV2yYegofO/SUXruT+pn4KxkxmaQ++1B/QedcKBFM=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.65/go.mod h1:4zyjAuGOdikpNYiSGpsGz8hLGmUzlY8pc8r9QQ/RXYQ=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 h1:x793wxmUWVDhshP8WW2mlnXuFrO4cOd3HLBroh1paFw=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30/go.mod h1:Jpne2tDnYiFascUEs2AWHJL9Yp7A5ZVy3TNyxaAjD6M=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo=
 github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.8.1/go.mod h1:CM+19rL1+4dFWnOQKwDc7H1KwXTz+h61oUSHyhV0b3o=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14 h1:2scbY6//jy/s8+5vGrk7l1+UtHl0h9A4MjOO2k/TM2E=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.14/go.mod h1:bRpZPHZpSe5YRHmPfK3h1M7UBFCn2szHzyx0rw04zro=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.16 h1:Wg+SyAmJFupMcHW9CHn2QK0M5nksu8JeXWVJIRVL8Nk=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.42.16/go.mod h1:t2tzigPR3e5R46iVnpIQrfVbA9AIuy5VLYqyk3gffjg=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.48.8 h1:abeu0IVRqYXSts7Tl1Yoi/BxC59xdXYX0uVSN0fbPOk=
-github.com/aws/aws-sdk-go-v2/service/route53 v1.48.8/go.mod h1:bOsuAIYHQbL+AqCldJ286MeljQL1sjUVGlpz9JMxCRM=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.16 h1:YV6xIKDJp6U7YB2bxfud9IENO1LRpGhe2Tv/OKtPrOQ=
-github.com/aws/aws-sdk-go-v2/service/sso v1.24.16/go.mod h1:DvbmMKgtpA6OihFJK13gHMZOZrCHttz8wPHGKXqU+3o=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15 h1:kMyK3aKotq1aTBsj1eS8ERJLjqYRRRcsmP33ozlCvlk=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.15/go.mod h1:5uPZU7vSNzb8Y0dm75xTikinegPYK3uJmIHQZFq5Aqo=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.15 h1:ht1jVmeeo2anR7zDiYJLSnRYnO/9NILXXu42FP3rJg0=
-github.com/aws/aws-sdk-go-v2/service/sts v1.33.15/go.mod h1:xWZ5cOiFe3czngChE4LhCBqUxNwgfwndEF7XlYP/yD8=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1 h1:0j58UseBtLuBcP6nY2z4SM1qZEvLF0ylyH6+ggnphLg=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.43.1/go.mod h1:Qy22QnQSdHbZwMZrarsWZBIuK51isPlkD+Z4sztxX0o=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 h1:/nkJHXtJXJeelXHqG0898+fWKgvfaXBhGzbCsSmn9j8=
+github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0/go.mod h1:kGYOjvTa0Vw0qxrqrOLut1vMnui6qLxqv/SX3vYeM8Y=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.2 h1:pdgODsAhGo4dvzC3JAG5Ce0PX8kWXrTZGx+jxADD+5E=
+github.com/aws/aws-sdk-go-v2/service/sso v1.25.2/go.mod h1:qs4a9T5EMLl/Cajiw2TcbNt2UNo/Hqlyp+GiuG4CFDI=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0 h1:90uX0veLKcdHVfvxhkWUQSCi5VabtwMLFutYiRke4oo=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.0/go.mod h1:MlYRNmYu/fGPoxBQVvBYr9nyr948aY/WLUvwBMBJubs=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 h1:PZV5W8yk4OtH1JAuhV2PXwwO9v5G5Aoj+eMCn4T+1Kc=
+github.com/aws/aws-sdk-go-v2/service/sts v1.33.17/go.mod h1:cQnB8CUnxbMU82JvlqjKR2HBOm3fe9pWorWBza6MBJ4=
 github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
 github.com/aws/smithy-go v1.22.3 h1:Z//5NuZCSW6R4PhQ93hShNbyBbn8BWCmCVCt+Q8Io5k=
 github.com/aws/smithy-go v1.22.3/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI=
@@ -765,13 +763,9 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
 github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
 github.com/bsm/redislock v0.9.4 h1:X/Wse1DPpiQgHbVYRE9zv6m070UcKoOGekgvpNhiSvw=
 github.com/bsm/redislock v0.9.4/go.mod h1:Epf7AJLiSFwLCiZcfi6pWFO/8eAYrYpQXFxEDPoDeAk=
-github.com/bytedance/sonic v1.12.9 h1:Od1BvK55NnewtGaJsTDeAOSnLVO2BTSLOe0+ooKokmQ=
-github.com/bytedance/sonic v1.12.9/go.mod h1:uVvFidNmlt9+wa31S1urfwwthTWteBgG0hWuoKAXTx8=
-github.com/bytedance/sonic v1.13.1 h1:Jyd5CIvdFnkOWuKXr+wm4Nyk2h0yAFsr8ucJgEasO3g=
-github.com/bytedance/sonic v1.13.1/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
+github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
+github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
 github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
-github.com/bytedance/sonic/loader v0.2.3 h1:yctD0Q3v2NOGfSWPLPvG2ggA2kV6TS6s4wioyEqssH0=
-github.com/bytedance/sonic/loader v0.2.3/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
 github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
 github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
 github.com/c-bata/go-prompt v0.2.5/go.mod h1:vFnjEGDIIA/Lib7giyE4E9c50Lvl8j0S+7FVlAwDAVw=
@@ -779,8 +773,6 @@ github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdf
 github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA=
 github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U=
 github.com/casbin/casbin/v2 v2.37.0/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg=
-github.com/casdoor/casdoor-go-sdk v1.4.0 h1:EhnIcMeCPiDE66tedy6EISkVjndR78slnwXqTfUnyhU=
-github.com/casdoor/casdoor-go-sdk v1.4.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
 github.com/casdoor/casdoor-go-sdk v1.5.0 h1:mlKWG2NcQfpR1w+TyOtzPtupfgseuDMSqykP1gJq+g0=
 github.com/casdoor/casdoor-go-sdk v1.5.0/go.mod h1:cMnkCQJgMYpgAlgEx8reSt1AVaDIQLcJ1zk5pzBaz+4=
 github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw=
@@ -801,8 +793,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
 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.94 h1:VhdqaJ2m4z8Jz8arzyzVjokRnO8JQ3lGjLKLshJ1eJI=
-github.com/civo/civogo v0.3.94/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
+github.com/civo/civogo v0.3.96 h1:9R3yZS3B8B0oAqIlNDnMvsONk0mqZUkHREd0EH6HRIc=
+github.com/civo/civogo v0.3.96/go.mod h1:LaEbkszc+9nXSh4YNG0sYXFGYqdQFmXXzQg0gESs2hc=
 github.com/clbanning/mxj v1.8.4/go.mod h1:BVjHeAH+rl9rs6f+QIpeRl0tfu10SXn1pUSa5PVGJng=
 github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
 github.com/cloudflare/cloudflare-go v0.115.0 h1:84/dxeeXweCc0PN5Cto44iTA8AkG1fyT11yPO5ZB7sM=
@@ -882,8 +874,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
 github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
 github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w=
 github.com/envoyproxy/protoc-gen-validate v0.10.0/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss=
-github.com/exoscale/egoscale/v3 v3.1.9 h1:kC876X4GKsojoqzJtq/MxNG91ebrDVEM9Ro+XOL7Yts=
-github.com/exoscale/egoscale/v3 v3.1.9/go.mod h1:t9+MpSEam94na48O/xgvvPFpQPRiwZ3kBN4/UuQtKco=
+github.com/exoscale/egoscale/v3 v3.1.13 h1:CAGC7QRjp2AiGj01agsSD0VKCp4OZmW5f51vV2IguNQ=
+github.com/exoscale/egoscale/v3 v3.1.13/go.mod h1:t9+MpSEam94na48O/xgvvPFpQPRiwZ3kBN4/UuQtKco=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
 github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
@@ -926,10 +918,6 @@ github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T
 github.com/go-acme/lego/v4 v4.22.2 h1:ck+HllWrV/rZGeYohsKQ5iKNnU/WAZxwOdiu6cxky+0=
 github.com/go-acme/lego/v4 v4.22.2/go.mod h1:E2FndyI3Ekv0usNJt46mFb9LVpV/XBYT+4E3tz02Tzo=
 github.com/go-cmd/cmd v1.0.5/go.mod h1:y8q8qlK5wQibcw63djSl/ntiHUHXHGdCkPk0j4QeW4s=
-github.com/go-co-op/gocron/v2 v2.15.0 h1:Kpvo71VSihE+RImmpA+3ta5CcMhoRzMGw4dJawrj4zo=
-github.com/go-co-op/gocron/v2 v2.15.0/go.mod h1:ZF70ZwEqz0OO4RBXE1sNxnANy/zvwLcattWEFsqpKig=
-github.com/go-co-op/gocron/v2 v2.16.0 h1:uqUF6WFZ4enRU45pWFNcn1xpDLc+jBOTKhPQI16Z1xs=
-github.com/go-co-op/gocron/v2 v2.16.0/go.mod h1:opexeOFy5BplhsKdA7bzY9zeYih8I8/WNJ4arTIFPVc=
 github.com/go-co-op/gocron/v2 v2.16.1 h1:ux/5zxVRveCaCuTtNI3DiOk581KC1KpJbpJFYUEVYwo=
 github.com/go-co-op/gocron/v2 v2.16.1/go.mod h1:opexeOFy5BplhsKdA7bzY9zeYih8I8/WNJ4arTIFPVc=
 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@@ -943,10 +931,10 @@ github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmn
 github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-gormigrate/gormigrate/v2 v2.1.3 h1:ei3Vq/rpPI/jCJY9mRHJAKg5vU+EhZyWhBAkaAomQuw=
-github.com/go-gormigrate/gormigrate/v2 v2.1.3/go.mod h1:VJ9FIOBAur+NmQ8c4tDVwOuiJcgupTG105FexPFrXzA=
-github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
-github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
+github.com/go-gormigrate/gormigrate/v2 v2.1.4 h1:KOPEt27qy1cNzHfMZbp9YTmEuzkY4F4wrdsJW9WFk1U=
+github.com/go-gormigrate/gormigrate/v2 v2.1.4/go.mod h1:y/6gPAH6QGAgP1UfHMiXcqGeJ88/GRQbfCReE1JJD5Y=
+github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
+github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs=
@@ -976,13 +964,13 @@ 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.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
-github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
 github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
 github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
 github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
-github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
-github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
+github.com/go-sql-driver/mysql v1.9.1 h1:FrjNGn/BsJQjVRuSa8CBrM5BWA9BWoXXat3KrtSb/iI=
+github.com/go-sql-driver/mysql v1.9.1/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
 github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
@@ -990,12 +978,8 @@ 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.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
 github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
-github.com/go-webauthn/webauthn v0.12.1 h1:fQNKWc+gd7i1TW8FmlB0jQTHyc2GYYlV/QdLUxo+MSA=
-github.com/go-webauthn/webauthn v0.12.1/go.mod h1:Q13xKHZi459wU8IoFjm8jQ6CMRyad+kegblwMFFhQGU=
 github.com/go-webauthn/webauthn v0.12.2 h1:yLaNPgBUEXDQtWnOjhsGhMMCEWbXwjg/aNkC8riJQI8=
 github.com/go-webauthn/webauthn v0.12.2/go.mod h1:Q8SZPPj4sZ469fNTcQXxRpzJOdb30jQrn/36FX8jilA=
-github.com/go-webauthn/x v0.1.18 h1:9xxiKRKCHx/1R2RF+4xb1qY5QDIO0RlTmH5L02lmRH4=
-github.com/go-webauthn/x v0.1.18/go.mod h1:Q/uHdGGFrZ7psEcoEStYunurZuG3Z9UDZJetM8qDTtA=
 github.com/go-webauthn/x v0.1.19 h1:IUfdHiBRoTdujpBA/14qbrMXQ3LGzYe/PRGWdZcmudg=
 github.com/go-webauthn/x v0.1.19/go.mod h1:C5arLuTQ3pVHKPw89v7CDGnqAZSZJj+4Jnr40dsn7tk=
 github.com/go-zookeeper/zk v1.0.2/go.mod h1:nOB03cncLtlp4t+UAkGSV+9beXP/akpekBwL+UX1Qcw=
@@ -1017,10 +1001,8 @@ github.com/goji/httpauth v0.0.0-20160601135302-2da839ab0f4d/go.mod h1:nnjvkQ9ptG
 github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
 github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
-github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
-github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
-github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
 github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
 github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
@@ -1087,8 +1069,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
 github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
 github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
 github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
 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=
@@ -1135,8 +1117,8 @@ 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.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw=
-github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4=
+github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=
 github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
 github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
 github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
@@ -1221,7 +1203,6 @@ github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
 github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
-github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
 github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
@@ -1235,8 +1216,8 @@ github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKEN
 github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
 github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.136 h1:T785NUg5245nWpPVHLVR8lBd+zGQYR14Vi/TCX1iu3A=
-github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.136/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
+github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.142 h1:9iOJ8tfNLw8uSiR5yx7VcHEYSOajJq5hb9SXF0BCUdA=
+github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.142/go.mod h1:Y/+YLCFCJtS29i2MbYPTUlNNfwXvkzEsZKR0imY/2aY=
 github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo=
 github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
 github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -1359,8 +1340,6 @@ github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8
 github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
 github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
 github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
-github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY=
-github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8=
 github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
 github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
 github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
@@ -1396,8 +1375,8 @@ github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
 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.47.0 h1:6MFNCyzWbr8Rhl4r7d5DwZLwxvFIsM4ARH6W0KS/R0U=
-github.com/linode/linodego v1.47.0/go.mod h1:vyklQRzZUWhFVBZdYx4dcYJU/gG9yKB9VUcUs6ub0Lk=
+github.com/linode/linodego v1.48.1 h1:Ojw1S+K5jJr1dggO8/H6r4FINxXnJbOU5GkbpaTfmhU=
+github.com/linode/linodego v1.48.1/go.mod h1:fc3t60If8X+yZTFAebhCnNDFrhwQhq9HDU92WnBousQ=
 github.com/liquidweb/go-lwApi v0.0.0-20190605172801-52a4864d2738/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
 github.com/liquidweb/go-lwApi v0.0.5/go.mod h1:0sYF9rMXb0vlG+4SzdiGMXHheCZxjguMq+Zb4S2BfBs=
 github.com/liquidweb/liquidweb-cli v0.6.9/go.mod h1:cE1uvQ+x24NGUL75D0QagOFCG8Wdvmwu8aL9TLmA/eQ=
@@ -1405,16 +1384,14 @@ 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-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
-github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
+github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
+github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
 github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
 github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
 github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.4/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
-github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
-github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
 github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
 github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -1454,8 +1431,8 @@ github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKju
 github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=
 github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
 github.com/miekg/dns v1.1.47/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
-github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
-github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
+github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ=
+github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
 github.com/mimuret/golang-iij-dpf v0.9.1 h1:Gj6EhHJkOhr+q2RnvRPJsPMcjuVnWPSccEHyoEehU34=
 github.com/mimuret/golang-iij-dpf v0.9.1/go.mod h1:sl9KyOkESib9+KRD3HaGpgi1xk7eoN2+d96LCLsME2M=
 github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY=
@@ -1505,10 +1482,10 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uY
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
 github.com/nrdcg/auroradns v1.1.0 h1:KekGh8kmf2MNwqZVVYo/fw/ZONt8QMEmbMFOeljteWo=
 github.com/nrdcg/auroradns v1.1.0/go.mod h1:O7tViUZbAcnykVnrGkXzIJTHoQCHcgalgAe6X1mzHfk=
-github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 h1:ouZ2JWDl8IW5k1qugYbmpbmW8hn85Ig6buSMBRlz3KI=
-github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3/go.mod h1:ZwadWt7mVhMHMbAQ1w8IhDqtWO3eWqWq72W7trnaiE8=
-github.com/nrdcg/desec v0.10.0 h1:qrEDiqnsvNU9QE7lXIXi/tIHAfyaFXKxF2/8/52O8uM=
-github.com/nrdcg/desec v0.10.0/go.mod h1:5+4vyhMRTs49V9CNoODF/HwT8Mwxv9DJ6j+7NekUnBs=
+github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea h1:OSgRS4kqOs/WuxuFOObP2gwrenL4/qiKXQbQugr/Two=
+github.com/nrdcg/bunny-go v0.0.0-20250327222614-988a091fc7ea/go.mod h1:IDRRngAngb2eTEaWgpO0hukQFI/vJId46fT1KErMytA=
+github.com/nrdcg/desec v0.11.0 h1:XZVHy07sg12y8FozMp+l7XvzPsdzog0AYXuQMaHBsfs=
+github.com/nrdcg/desec v0.11.0/go.mod h1:5+4vyhMRTs49V9CNoODF/HwT8Mwxv9DJ6j+7NekUnBs=
 github.com/nrdcg/dnspod-go v0.4.0 h1:c/jn1mLZNKF3/osJ6mz3QPxTudvPArXTjpkmYj0uK6U=
 github.com/nrdcg/dnspod-go v0.4.0/go.mod h1:vZSoFSFeQVm2gWLMkyX61LZ8HI3BaqtHZWgPTGKr6KQ=
 github.com/nrdcg/freemyip v0.3.0 h1:0D2rXgvLwe2RRaVIjyUcQ4S26+cIS2iFwnhzDsEuuwc=
@@ -1555,8 +1532,8 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr
 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/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE=
-github.com/oracle/oci-go-sdk/v65 v65.83.2 h1:4DtSCVe/AaHcqb08wXgjplOM8+tc4pqNwcUYZmplbv8=
-github.com/oracle/oci-go-sdk/v65 v65.83.2/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
+github.com/oracle/oci-go-sdk/v65 v65.88.0 h1:SbsGKsoRRxJxVTbwUyIPCPwPsHWb8aPgEEpo6qfRJnI=
+github.com/oracle/oci-go-sdk/v65 v65.88.0/go.mod h1:IBEV9l1qBzUpo7zgGaRUhbB05BVfcDGYRFBCPlTcPp0=
 github.com/ovh/go-ovh v1.7.0 h1:V14nF7FwDjQrZt9g7jzcvAAQ3HN6DNShRFRMC3jLoPw=
 github.com/ovh/go-ovh v1.7.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c=
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
@@ -1632,10 +1609,6 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
-github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
-github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
-github.com/redis/go-redis/v9 v9.7.1 h1:4LhKRCIduqXqtvCUlaq9c8bdHOkICjDMrr1+Zb3osAc=
-github.com/redis/go-redis/v9 v9.7.1/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
 github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
 github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
 github.com/regfish/regfish-dnsapi-go v0.1.1 h1:TJFtbePHkd47q5GZwYl1h3DIYXmoxdLjW/SBsPtB5IE=
@@ -1668,28 +1641,20 @@ github.com/sacloud/iaas-api-go v1.14.0/go.mod h1:C8os2Mnj0TOmMdSllwhaDWKMVG2ysFn
 github.com/sacloud/packages-go v0.0.11 h1:hrRWLmfPM9w7GBs6xb5/ue6pEMl8t1UuDKyR/KfteHo=
 github.com/sacloud/packages-go v0.0.11/go.mod h1:XNF5MCTWcHo9NiqWnYctVbASSSZR3ZOmmQORIzcurJ8=
 github.com/sagikazarmark/crypt v0.10.0/go.mod h1:gwTNHQVoOS3xp9Xvz5LLR+1AauC5M6880z5NWzdhOyQ=
-github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
-github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
-github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
-github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
+github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
 github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
 github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
-github.com/sashabaranov/go-openai v1.37.0 h1:hQQowgYm4OXJ1Z/wTrE+XZaO20BYsL0R3uRPSpfNZkY=
-github.com/sashabaranov/go-openai v1.37.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
-github.com/sashabaranov/go-openai v1.38.0 h1:hNN5uolKwdbpiqOn7l+Z2alch/0n0rSFyg4n+GZxR5k=
-github.com/sashabaranov/go-openai v1.38.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
 github.com/sashabaranov/go-openai v1.38.1 h1:TtZabbFQZa1nEni/IhVtDF/WQjVqDgd+cWR5OeddzF8=
 github.com/sashabaranov/go-openai v1.38.1/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
 github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32 h1:4+LP7qmsLSGbmc66m1s5dKRMBwztRppfxFKlYqYte/c=
-github.com/scaleway/scaleway-sdk-go v1.0.0-beta.32/go.mod h1:kzh+BSAvpoyHHdHBCDhmSWtBc1NbLMZ2lWHqnBoxFks=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33 h1:KhF0WejiUTDbL5X55nXowP7zNopwpowa6qaMAWyIE+0=
+github.com/scaleway/scaleway-sdk-go v1.0.0-beta.33/go.mod h1:792k1RTU+5JeMXm35/e2Wgp71qPH/DmDoZrRc+EFZDk=
 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
 github.com/selectel/domains-go v1.1.0 h1:futG50J43ALLKQAnZk9H9yOtLGnSUh7c5hSvuC5gSHo=
 github.com/selectel/domains-go v1.1.0/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA=
 github.com/selectel/go-selvpcclient/v3 v3.2.1 h1:ny6WIAMiHzKxOgOEnwcWE79wIQij1AHHylzPA41MXCw=
 github.com/selectel/go-selvpcclient/v3 v3.2.1/go.mod h1:3EfSf8aEWyhspOGbvZ6mvnFg7JN5uckxNyBFPGWsXNQ=
-github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
-github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
 github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk=
 github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA=
 github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
@@ -1732,8 +1697,8 @@ github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z
 github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y=
 github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ=
-github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
-github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
+github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
+github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
@@ -1750,8 +1715,8 @@ github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg=
-github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
-github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
+github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
+github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
 github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 github.com/streadway/amqp v1.0.0/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
 github.com/streadway/handy v0.0.0-20200128134331-0f66f006fb2e/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
@@ -1781,21 +1746,22 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
 github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1101 h1:pz6QIjHR7TXQfEogg4pwvvTDgsB1L+RQGgnr2tBDzc4=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1101/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1101 h1:9c05Ky7Ppww06YFE579TjI89pfNnC2zdJufx7SXUTi8=
-github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1101/go.mod h1:fBdcH58lmwIwePei24b9QFdE1w8+brIX9yTrf82n7yM=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1128/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1133 h1:S+ZHcAfI8+ii4MfsCr41R3CdhlTsc5OddGsCfeYJdl8=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1133/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128 h1:mrJ5Fbkd7sZIJ5F6oRfh5zebPQaudPH9Y0+GUmFytYU=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128/go.mod h1:zbsYIBT+VTX4z4ocjTAdLBIWyNYj3z0BRqd0iPdnjsk=
 github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
 github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
-github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
-github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
-github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
-github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
+github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
+github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
+github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
+github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 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/tufanbarisyildirim/gonginx v0.0.0-20250120210832-12a9c7ae0c8a h1:bFMBmB409YhHd+B4yfM7JPYTCgxnQjqzIfFOiN4Tpsc=
-github.com/tufanbarisyildirim/gonginx v0.0.0-20250120210832-12a9c7ae0c8a/go.mod h1:hdMWBc1+TyB6G5ZZBBgPWQ8cjRZ6zpYdhal0uu6E9QM=
+github.com/tufanbarisyildirim/gonginx v0.0.0-20250225174229-c03497ddaef6 h1:HmtcQ7w07RI2SdTKkPf+NM8R33B1oR9MjIZYzlBizwA=
+github.com/tufanbarisyildirim/gonginx v0.0.0-20250225174229-c03497ddaef6/go.mod h1:hdMWBc1+TyB6G5ZZBBgPWQ8cjRZ6zpYdhal0uu6E9QM=
 github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
@@ -1807,16 +1773,8 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E
 github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
 github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec h1:2s/ghQ8wKE+UzD/hf3P4Gd1j0JI9ncbxv+nsypPoUYI=
 github.com/ultradns/ultradns-go-sdk v1.8.0-20241010134910-243eeec/go.mod h1:BZr7Qs3ku1ckpqed8tCRSqTlp8NAeZfAVpfx4OzXMss=
-github.com/uozi-tech/cosy v1.14.4 h1:9X9CzxYjTg9DRQKgBjYvDNOAYYFclOXYYq518nO4vr0=
-github.com/uozi-tech/cosy v1.14.4/go.mod h1:DSKLtoVaGLUlJ8KiQ1vWEsnv85epRrAAMXSijuq+asM=
-github.com/uozi-tech/cosy v1.14.5 h1:hZ4wGf+558myDayc/KtCVv6MDCwd2pE6q6AUeF+EKI8=
-github.com/uozi-tech/cosy v1.14.5/go.mod h1:KWo+XpzLiO3EUWJkXT7ca4nxX+vDVH0eB0B1BrOBkqg=
-github.com/uozi-tech/cosy v1.15.5 h1:rSnFYUzJQreKeFSyOEPTjv7PknSFjO5m2mB/jV2Z32s=
-github.com/uozi-tech/cosy v1.15.5/go.mod h1:b6VfiTeaIzMOKeUdjGz6cC2Nu3hnPLJfHsLe8zmfjvE=
-github.com/uozi-tech/cosy v1.15.6 h1:IMU3Gf0mlmi7CF87Ujnyc8Iu7wdv0eEE+olDOuNPZz8=
-github.com/uozi-tech/cosy v1.15.6/go.mod h1:b6VfiTeaIzMOKeUdjGz6cC2Nu3hnPLJfHsLe8zmfjvE=
-github.com/uozi-tech/cosy v1.16.0 h1:hKFM8sOaedzaRCuHM1EnY8q0BAvsvFVDhTrJ3IHZEk0=
-github.com/uozi-tech/cosy v1.16.0/go.mod h1:jEyznv+lmbb0YO0gU//yn4PnyqncTlyV2H5BpDa5aEw=
+github.com/uozi-tech/cosy v1.17.0 h1:qrdBhbylsHGIOUcUsZKUdVzq8fLvePIclHVSGdszyxk=
+github.com/uozi-tech/cosy v1.17.0/go.mod h1:jEyznv+lmbb0YO0gU//yn4PnyqncTlyV2H5BpDa5aEw=
 github.com/uozi-tech/cosy-driver-mysql v0.2.2 h1:22S/XNIvuaKGqxQPsYPXN8TZ8hHjCQdcJKVQ83Vzxoo=
 github.com/uozi-tech/cosy-driver-mysql v0.2.2/go.mod h1:EZnRIbSj1V5U0gEeTobrXai/d1SV11lkl4zP9NFEmyE=
 github.com/uozi-tech/cosy-driver-postgres v0.2.1 h1:OICakGuT+omva6QOJCxTJ5Lfr7CGXLmk/zD+aS51Z2o=
@@ -1828,10 +1786,10 @@ github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjc
 github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y=
 github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
 github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
-github.com/volcengine/volc-sdk-golang v1.0.195 h1:hKX4pBhmKcB3652BTdcAmtgizEPBnoQUpTM+j5blMA4=
-github.com/volcengine/volc-sdk-golang v1.0.195/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
-github.com/vultr/govultr/v3 v3.14.1 h1:9BpyZgsWasuNoR39YVMcq44MSaF576Z4D+U3ro58eJQ=
-github.com/vultr/govultr/v3 v3.14.1/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
+github.com/volcengine/volc-sdk-golang v1.0.201 h1:AnKtLpuEGCLuH9Yd2TvhG0SeTa+u4+MpLotIMZCdBgU=
+github.com/volcengine/volc-sdk-golang v1.0.201/go.mod h1:stZX+EPgv1vF4nZwOlEe8iGcriUPRBKX8zA19gXycOQ=
+github.com/vultr/govultr/v3 v3.18.0 h1:nTfxZW7/BRUDdZyEDSWzqrtyQgNolFPXBlwwJuM7EF8=
+github.com/vultr/govultr/v3 v3.18.0/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
 github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
 github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
 github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
@@ -1843,10 +1801,10 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
-github.com/yandex-cloud/go-genproto v0.0.0-20250217111757-ccaea642a16c h1:WTK2XiEf68Uv0rT6mjrB5hKkwZvMnWWHPF3OjK/fYL8=
-github.com/yandex-cloud/go-genproto v0.0.0-20250217111757-ccaea642a16c/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
-github.com/yandex-cloud/go-sdk v0.0.0-20250210144447-399a857b9c4e h1:RiNKkceZPeMWLSIl31RSgPeSmpT9K7eTXOcA9YxTBfg=
-github.com/yandex-cloud/go-sdk v0.0.0-20250210144447-399a857b9c4e/go.mod h1:OCW2kKPZ900GNQ9aKDaX7/FUQmxGdm+CKeXVocbM4d0=
+github.com/yandex-cloud/go-genproto v0.0.0-20250325081613-cd85d9003939 h1:o1L5uP1z/IKGQpfzEqSmqGtFDIKDoFAvZuqpzySIVFc=
+github.com/yandex-cloud/go-genproto v0.0.0-20250325081613-cd85d9003939/go.mod h1:0LDD/IZLIUIV4iPH+YcF+jysO3jkSvADFGm4dCAuwQo=
+github.com/yandex-cloud/go-sdk v0.0.0-20250325134853-dcb34ef70818 h1:EgfskqIEIv/f5vx/guwfkakNwy5H9Mm+OC17zS1ofus=
+github.com/yandex-cloud/go-sdk v0.0.0-20250325134853-dcb34ef70818/go.mod h1:U2Cc0SZ8kQHcL4ffnfNN78bdSybVP2pQNq0oJfFwvM8=
 github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -1872,8 +1830,8 @@ go.etcd.io/etcd/client/v2 v2.305.7/go.mod h1:GQGT5Z3TBuAQGvgPfhR7VPySu/SudxmEkRq
 go.etcd.io/etcd/client/v3 v3.5.0/go.mod h1:AIKXXVX/DQXtfTEqBryiLTUXwON+GuvO6Z7lLS/oTh0=
 go.etcd.io/etcd/client/v3 v3.5.9/go.mod h1:i/Eo5LrZ5IKqpbtpPDuaUnDOUv471oDg8cjQaUr2MbA=
 go.mongodb.org/mongo-driver v1.13.1/go.mod h1:wcDf1JBCXy2mOW0bWHwO/IOYqdca1MPCwDtFu/Z9+eo=
-go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM=
-go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
+go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
+go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -1884,18 +1842,18 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
 go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
-go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
-go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
-go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
-go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU=
+go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
 go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=
@@ -1930,8 +1888,6 @@ go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
 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.14.0 h1:z9JUEZWr8x4rR0OU6c4/4t6E6jOZ8/QBS2bBYBm4tx4=
-golang.org/x/arch v0.14.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
 golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -1970,10 +1926,6 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY
 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.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
-golang.org/x/crypto v0.34.0 h1:+/C6tk6rf/+t5DhUketUbD1aNGqiSX3j15Z6xuIDlBA=
-golang.org/x/crypto v0.34.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
-golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
-golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
 golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
 golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
 golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@@ -1991,8 +1943,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-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
-golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -2035,8 +1985,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.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM=
-golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
 golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
 golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -2118,10 +2066,8 @@ 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.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
-golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
-golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
-golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
-golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
+golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
+golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -2151,8 +2097,8 @@ 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.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
-golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
+golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
+golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -2169,8 +2115,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.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
-golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
 golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -2294,8 +2238,6 @@ golang.org/x/sys v0.10.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.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
 golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -2313,8 +2255,8 @@ golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
 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.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
-golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
+golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
+golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
 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=
@@ -2334,8 +2276,6 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
-golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
 golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
 golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -2346,8 +2286,8 @@ golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxb
 golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
-golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
+golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -2422,8 +2362,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.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
-golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
 golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
 golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -2502,8 +2440,8 @@ 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.221.0 h1:qzaJfLhDsbMeFee8zBRdt/Nc+xmOuafD/dbdgGfutOU=
-google.golang.org/api v0.221.0/go.mod h1:7sOU2+TL4TxUTdbi0gWgAIg7tH5qBXxoyhtL+9x3biQ=
+google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs=
+google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4=
 google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -2644,12 +2582,12 @@ 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-20250218202821-56aae31c358a h1:Xx6e5r1AOINOgm2ZuzvwDueGlOOml4PKBUry8jqyS6U=
-google.golang.org/genproto v0.0.0-20250218202821-56aae31c358a/go.mod h1:Cmg1ztsSOnOsWxOiPTOUX8gegyHg5xADRncIHdtec8U=
-google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0=
-google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ=
+google.golang.org/genproto v0.0.0-20250324211829-b45e905df463 h1:qEFnJI6AnfZk0NNe8YTyXQh5i//Zxi4gBHwRgp76qpw=
+google.golang.org/genproto v0.0.0-20250324211829-b45e905df463/go.mod h1:SqIx1NV9hcvqdLHo7uNZDS5lrUJybQ3evo3+z/WBfA0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
+google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -2692,8 +2630,8 @@ 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.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
-google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
+google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
+google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
 google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
 google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -2712,8 +2650,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw
 google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
 google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
-google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -2725,8 +2663,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
 gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o=
-gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg=
-gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI=
 gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0=
 gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
 gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
@@ -2793,14 +2729,14 @@ 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.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw=
-k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y=
-k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ=
-k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
+k8s.io/api v0.32.3 h1:Hw7KqxRusq+6QSplE3NYG4MBxZw1BZnq4aP4cJVINls=
+k8s.io/api v0.32.3/go.mod h1:2wEDTXADtm/HA7CCMD8D8bK4yuBUptzaRhYcYEEYA3k=
+k8s.io/apimachinery v0.32.3 h1:JmDuDarhDmA/Li7j3aPrwhpNBA94Nvk5zLeOge9HH1U=
+k8s.io/apimachinery v0.32.3/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
 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-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
-k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
+k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e h1:KqK5c/ghOm8xkHYhlodbp6i6+r+ChV2vuAuVRdFbLro=
+k8s.io/utils v0.0.0-20250321185631-1f6e0b77f77e/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
 lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
 modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
@@ -2842,8 +2778,9 @@ rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
 sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
 sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
-sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk=
-sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
+sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
+sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
+sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
 sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
 sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
 sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

+ 169 - 0
internal/backup/backup.go

@@ -0,0 +1,169 @@
+package backup
+
+import (
+	"bytes"
+	"encoding/base64"
+	"fmt"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/0xJacky/Nginx-UI/internal/version"
+	"github.com/uozi-tech/cosy"
+	"github.com/uozi-tech/cosy/logger"
+)
+
+// Directory and file names
+const (
+	BackupDirPrefix = "nginx-ui-backup-"
+	NginxUIDir      = "nginx-ui"
+	NginxDir        = "nginx"
+	HashInfoFile    = "hash_info.txt"
+	NginxUIZipName  = "nginx-ui.zip"
+	NginxZipName    = "nginx.zip"
+)
+
+// BackupResult contains the results of a backup operation
+type BackupResult struct {
+	BackupContent []byte `json:"-"`       // Backup content as byte array
+	BackupName    string `json:"name"`    // Backup file name
+	AESKey        string `json:"aes_key"` // Base64 encoded AES key
+	AESIv         string `json:"aes_iv"`  // Base64 encoded AES IV
+}
+
+// HashInfo contains hash information for verification
+type HashInfo struct {
+	NginxUIHash string `json:"nginx_ui_hash"`
+	NginxHash   string `json:"nginx_hash"`
+	Timestamp   string `json:"timestamp"`
+	Version     string `json:"version"`
+}
+
+// Backup creates a backup of nginx-ui configuration and database files,
+// and nginx configuration directory, compressed into an encrypted archive
+func Backup() (BackupResult, error) {
+	// Generate timestamps for filenames
+	timestamp := time.Now().Format("20060102-150405")
+	backupName := fmt.Sprintf("backup-%s.zip", timestamp)
+
+	// Generate AES key and IV
+	key, err := GenerateAESKey()
+	if err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrGenerateAESKey, err.Error())
+	}
+
+	iv, err := GenerateIV()
+	if err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrGenerateIV, err.Error())
+	}
+
+	// Create temporary directory for files to be archived
+	tempDir, err := os.MkdirTemp("", "nginx-ui-backup-*")
+	if err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateTempDir, err.Error())
+	}
+	defer os.RemoveAll(tempDir)
+
+	// Create directories in temp
+	nginxUITempDir := filepath.Join(tempDir, NginxUIDir)
+	nginxTempDir := filepath.Join(tempDir, NginxDir)
+	if err := os.MkdirAll(nginxUITempDir, 0755); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateTempSubDir, err.Error())
+	}
+	if err := os.MkdirAll(nginxTempDir, 0755); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateTempSubDir, err.Error())
+	}
+
+	// Backup nginx-ui config and database to a directory
+	if err := backupNginxUIFiles(nginxUITempDir); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrBackupNginxUI, err.Error())
+	}
+
+	// Backup nginx configs to a directory
+	if err := backupNginxFiles(nginxTempDir); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrBackupNginx, err.Error())
+	}
+
+	// Create individual zip files for nginx-ui and nginx directories
+	nginxUIZipPath := filepath.Join(tempDir, NginxUIZipName)
+	nginxZipPath := filepath.Join(tempDir, NginxZipName)
+
+	// Create zip archives for each directory
+	if err := createZipArchive(nginxUIZipPath, nginxUITempDir); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateZipArchive, err.Error())
+	}
+
+	if err := createZipArchive(nginxZipPath, nginxTempDir); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateZipArchive, err.Error())
+	}
+
+	// Calculate hashes for the zip files
+	nginxUIHash, err := calculateFileHash(nginxUIZipPath)
+	if err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrCalculateHash, err.Error())
+	}
+
+	nginxHash, err := calculateFileHash(nginxZipPath)
+	if err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrCalculateHash, err.Error())
+	}
+
+	// Get current version information
+	versionInfo := version.GetVersionInfo()
+
+	// Create hash info file
+	hashInfo := HashInfo{
+		NginxUIHash: nginxUIHash,
+		NginxHash:   nginxHash,
+		Timestamp:   timestamp,
+		Version:     versionInfo.Version,
+	}
+
+	// Write hash info to file
+	hashInfoPath := filepath.Join(tempDir, HashInfoFile)
+	if err := writeHashInfoFile(hashInfoPath, hashInfo); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateHashFile, err.Error())
+	}
+
+	// Encrypt the individual files
+	if err := encryptFile(hashInfoPath, key, iv); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrEncryptFile, HashInfoFile)
+	}
+
+	if err := encryptFile(nginxUIZipPath, key, iv); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrEncryptNginxUIDir, err.Error())
+	}
+
+	if err := encryptFile(nginxZipPath, key, iv); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrEncryptNginxDir, err.Error())
+	}
+
+	// Remove the original directories to avoid duplicating them in the final archive
+	if err := os.RemoveAll(nginxUITempDir); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrCleanupTempDir, err.Error())
+	}
+	if err := os.RemoveAll(nginxTempDir); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrCleanupTempDir, err.Error())
+	}
+
+	// Create final zip file to memory buffer
+	var buffer bytes.Buffer
+	if err := createZipArchiveToBuffer(&buffer, tempDir); err != nil {
+		return BackupResult{}, cosy.WrapErrorWithParams(ErrCreateZipArchive, err.Error())
+	}
+
+	// Convert AES key and IV to base64 encoded strings
+	keyBase64 := base64.StdEncoding.EncodeToString(key)
+	ivBase64 := base64.StdEncoding.EncodeToString(iv)
+
+	// Return result
+	result := BackupResult{
+		BackupContent: buffer.Bytes(),
+		BackupName:    backupName,
+		AESKey:        keyBase64,
+		AESIv:         ivBase64,
+	}
+
+	logger.Infof("Backup created successfully: %s", backupName)
+	return result, nil
+}

+ 128 - 0
internal/backup/backup_crypto.go

@@ -0,0 +1,128 @@
+package backup
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/rand"
+	"encoding/base64"
+	"io"
+	"os"
+
+	"github.com/uozi-tech/cosy"
+)
+
+// AESEncrypt encrypts data using AES-256-CBC
+func AESEncrypt(data []byte, key []byte, iv []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, cosy.WrapErrorWithParams(ErrEncryptData, err.Error())
+	}
+
+	// Pad data to be a multiple of block size
+	padding := aes.BlockSize - (len(data) % aes.BlockSize)
+	padtext := make([]byte, len(data)+padding)
+	copy(padtext, data)
+	// PKCS#7 padding
+	for i := len(data); i < len(padtext); i++ {
+		padtext[i] = byte(padding)
+	}
+
+	// Create CBC encrypter
+	mode := cipher.NewCBCEncrypter(block, iv)
+	encrypted := make([]byte, len(padtext))
+	mode.CryptBlocks(encrypted, padtext)
+
+	return encrypted, nil
+}
+
+// AESDecrypt decrypts data using AES-256-CBC
+func AESDecrypt(encrypted []byte, key []byte, iv []byte) ([]byte, error) {
+	block, err := aes.NewCipher(key)
+	if err != nil {
+		return nil, cosy.WrapErrorWithParams(ErrDecryptData, err.Error())
+	}
+
+	// Create CBC decrypter
+	mode := cipher.NewCBCDecrypter(block, iv)
+	decrypted := make([]byte, len(encrypted))
+	mode.CryptBlocks(decrypted, encrypted)
+
+	// Remove padding
+	padding := int(decrypted[len(decrypted)-1])
+	if padding < 1 || padding > aes.BlockSize {
+		return nil, ErrInvalidPadding
+	}
+	return decrypted[:len(decrypted)-padding], nil
+}
+
+// GenerateAESKey generates a random 32-byte AES key
+func GenerateAESKey() ([]byte, error) {
+	key := make([]byte, 32) // 256-bit key
+	if _, err := io.ReadFull(rand.Reader, key); err != nil {
+		return nil, cosy.WrapErrorWithParams(ErrGenerateAESKey, err.Error())
+	}
+	return key, nil
+}
+
+// GenerateIV generates a random 16-byte initialization vector
+func GenerateIV() ([]byte, error) {
+	iv := make([]byte, aes.BlockSize)
+	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
+		return nil, cosy.WrapErrorWithParams(ErrGenerateIV, err.Error())
+	}
+	return iv, nil
+}
+
+// encryptFile encrypts a single file using AES encryption
+func encryptFile(filePath string, key []byte, iv []byte) error {
+	// Read file content
+	data, err := os.ReadFile(filePath)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrReadFile, filePath)
+	}
+
+	// Encrypt file content
+	encrypted, err := AESEncrypt(data, key, iv)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrEncryptFile, filePath)
+	}
+
+	// Write encrypted content back
+	if err := os.WriteFile(filePath, encrypted, 0644); err != nil {
+		return cosy.WrapErrorWithParams(ErrWriteEncryptedFile, filePath)
+	}
+
+	return nil
+}
+
+// decryptFile decrypts a single file using AES decryption
+func decryptFile(filePath string, key []byte, iv []byte) error {
+	// Read encrypted file content
+	encryptedData, err := os.ReadFile(filePath)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrReadEncryptedFile, err.Error())
+	}
+
+	// Decrypt file content
+	decryptedData, err := AESDecrypt(encryptedData, key, iv)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrDecryptFile, err.Error())
+	}
+
+	// Write decrypted content back
+	if err := os.WriteFile(filePath, decryptedData, 0644); err != nil {
+		return cosy.WrapErrorWithParams(ErrWriteDecryptedFile, err.Error())
+	}
+
+	return nil
+}
+
+// EncodeToBase64 encodes byte slice to base64 string
+func EncodeToBase64(data []byte) string {
+	return base64.StdEncoding.EncodeToString(data)
+}
+
+// DecodeFromBase64 decodes base64 string to byte slice
+func DecodeFromBase64(encoded string) ([]byte, error) {
+	return base64.StdEncoding.DecodeString(encoded)
+}

+ 76 - 0
internal/backup/backup_nginx_ui.go

@@ -0,0 +1,76 @@
+package backup
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"github.com/0xJacky/Nginx-UI/settings"
+	"github.com/uozi-tech/cosy"
+	"github.com/uozi-tech/cosy/logger"
+	cosysettings "github.com/uozi-tech/cosy/settings"
+)
+
+// backupNginxUIFiles backs up the nginx-ui configuration and database files
+func backupNginxUIFiles(destDir string) error {
+	// Get config file path
+	configPath := cosysettings.ConfPath
+	if configPath == "" {
+		return ErrConfigPathEmpty
+	}
+
+	// Always save the config file as app.ini, regardless of its original name
+	destConfigPath := filepath.Join(destDir, "app.ini")
+	if err := copyFile(configPath, destConfigPath); err != nil {
+		return cosy.WrapErrorWithParams(ErrCopyConfigFile, err.Error())
+	}
+
+	// Get database file name and path
+	dbName := settings.DatabaseSettings.GetName()
+	dbFile := dbName + ".db"
+
+	// Database directory is the same as config file directory
+	dbDir := filepath.Dir(configPath)
+	dbPath := filepath.Join(dbDir, dbFile)
+
+	// Copy database file
+	if _, err := os.Stat(dbPath); err == nil {
+		// Database exists as file
+		destDBPath := filepath.Join(destDir, dbFile)
+		if err := copyFile(dbPath, destDBPath); err != nil {
+			return cosy.WrapErrorWithParams(ErrCopyDBFile, err.Error())
+		}
+	} else {
+		logger.Warn("Database file not found: %s", dbPath)
+	}
+
+	return nil
+}
+
+// backupNginxFiles backs up the nginx configuration directory
+func backupNginxFiles(destDir string) error {
+	// Get nginx config directory
+	nginxConfigDir := settings.NginxSettings.ConfigDir
+	if nginxConfigDir == "" {
+		return ErrNginxConfigDirEmpty
+	}
+
+	// Copy nginx config directory
+	if err := copyDirectory(nginxConfigDir, destDir); err != nil {
+		return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, err.Error())
+	}
+
+	return nil
+}
+
+// writeHashInfoFile creates a hash information file for verification
+func writeHashInfoFile(hashFilePath string, info HashInfo) error {
+	content := fmt.Sprintf("nginx-ui_hash: %s\nnginx_hash: %s\ntimestamp: %s\nversion: %s\n",
+		info.NginxUIHash, info.NginxHash, info.Timestamp, info.Version)
+
+	if err := os.WriteFile(hashFilePath, []byte(content), 0644); err != nil {
+		return cosy.WrapErrorWithParams(ErrCreateHashFile, err.Error())
+	}
+
+	return nil
+}

+ 466 - 0
internal/backup/backup_test.go

@@ -0,0 +1,466 @@
+package backup
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/0xJacky/Nginx-UI/settings"
+	"github.com/stretchr/testify/assert"
+	cosylogger "github.com/uozi-tech/cosy/logger"
+	cosysettings "github.com/uozi-tech/cosy/settings"
+)
+
+func init() {
+	// Initialize logging system to avoid nil pointer exceptions during tests
+	cosylogger.Init("debug")
+
+	// Clean up backup files at the start of tests
+	cleanupBackupFiles()
+}
+
+// cleanupBackupFiles removes all backup files in the current directory
+func cleanupBackupFiles() {
+	// Get current directory
+	dir, err := os.Getwd()
+	if err != nil {
+		return
+	}
+
+	// Delete all backup files
+	matches, err := filepath.Glob(filepath.Join(dir, "backup-*.zip"))
+	if err == nil {
+		for _, file := range matches {
+			os.Remove(file)
+		}
+	}
+}
+
+// setupTestEnvironment creates a temporary environment for testing
+func setupTestEnvironment(t *testing.T) (string, func()) {
+	// Create temporary test directory
+	tempDir, err := os.MkdirTemp("", "backup-test-*")
+	assert.NoError(t, err)
+
+	// Set up necessary directories
+	nginxDir := filepath.Join(tempDir, "nginx")
+	nginxUIDir := filepath.Join(tempDir, "nginx-ui")
+	configDir := filepath.Join(tempDir, "config")
+	backupDir := filepath.Join(tempDir, "backup")
+
+	// Create directories
+	for _, dir := range []string{nginxDir, nginxUIDir, configDir, backupDir} {
+		err = os.MkdirAll(dir, 0755)
+		assert.NoError(t, err)
+	}
+
+	// Create some test files
+	testFiles := map[string]string{
+		filepath.Join(nginxDir, "nginx.conf"):    "user nginx;\nworker_processes auto;\n",
+		filepath.Join(nginxUIDir, "config.json"): `{"version": "1.0", "settings": {"theme": "dark"}}`,
+	}
+
+	for file, content := range testFiles {
+		err = os.WriteFile(file, []byte(content), 0644)
+		assert.NoError(t, err)
+	}
+
+	// Save original configuration
+	origNginxConfigDir := settings.NginxSettings.ConfigDir
+	origNginxUIConfigPath := cosysettings.ConfPath
+
+	// Set test configuration
+	settings.NginxSettings.ConfigDir = nginxDir
+	cosysettings.ConfPath = filepath.Join(configDir, "config.ini")
+
+	// Return cleanup function
+	cleanup := func() {
+		// Restore original configuration
+		settings.NginxSettings.ConfigDir = origNginxConfigDir
+		cosysettings.ConfPath = origNginxUIConfigPath
+
+		// Delete temporary directory
+		os.RemoveAll(tempDir)
+	}
+
+	return tempDir, cleanup
+}
+
+// Test backup and restore functionality
+func TestBackupAndRestore(t *testing.T) {
+	// Make sure backup files are cleaned up at the start and end of the test
+	cleanupBackupFiles()
+	defer cleanupBackupFiles()
+
+	// Create test configuration
+	tempDir, err := os.MkdirTemp("", "nginx-ui-backup-test-*")
+	assert.NoError(t, err)
+	defer os.RemoveAll(tempDir)
+
+	// Create config file
+	configPath := filepath.Join(tempDir, "config.ini")
+	testConfig := []byte("[app]\nName = Nginx UI Test\n")
+	err = os.WriteFile(configPath, testConfig, 0644)
+	assert.NoError(t, err)
+
+	// Create database file
+	dbName := settings.DatabaseSettings.GetName()
+	dbFile := dbName + ".db"
+	dbPath := filepath.Join(tempDir, dbFile)
+	testDB := []byte("CREATE TABLE users (id INT, name TEXT);")
+	err = os.WriteFile(dbPath, testDB, 0644)
+	assert.NoError(t, err)
+
+	// Create nginx directory
+	nginxConfigDir := filepath.Join(tempDir, "nginx")
+	err = os.MkdirAll(nginxConfigDir, 0755)
+	assert.NoError(t, err)
+
+	// Create test nginx config
+	testNginxContent := []byte("server {\n    listen 80;\n    server_name example.com;\n}\n")
+	err = os.WriteFile(filepath.Join(nginxConfigDir, "nginx.conf"), testNginxContent, 0644)
+	assert.NoError(t, err)
+
+	// Setup settings for testing
+	originalConfPath := cosysettings.ConfPath
+	originalNginxConfigDir := settings.NginxSettings.ConfigDir
+
+	cosysettings.ConfPath = configPath
+	settings.NginxSettings.ConfigDir = nginxConfigDir
+
+	// Restore original settings after test
+	defer func() {
+		cosysettings.ConfPath = originalConfPath
+		settings.NginxSettings.ConfigDir = originalNginxConfigDir
+	}()
+
+	// Run backup
+	result, err := Backup()
+	assert.NoError(t, err)
+	assert.NotEmpty(t, result.BackupContent)
+	assert.NotEmpty(t, result.BackupName)
+	assert.NotEmpty(t, result.AESKey)
+	assert.NotEmpty(t, result.AESIv)
+
+	// Save backup content to a temporary file for restore testing
+	backupPath := filepath.Join(tempDir, result.BackupName)
+	err = os.WriteFile(backupPath, result.BackupContent, 0644)
+	assert.NoError(t, err)
+
+	// Test restore functionality
+	restoreDir, err := os.MkdirTemp("", "nginx-ui-restore-test-*")
+	assert.NoError(t, err)
+	defer os.RemoveAll(restoreDir)
+
+	// Decode AES key and IV
+	aesKey, err := DecodeFromBase64(result.AESKey)
+	assert.NoError(t, err)
+	aesIv, err := DecodeFromBase64(result.AESIv)
+	assert.NoError(t, err)
+
+	// Perform restore
+	restoreResult, err := Restore(RestoreOptions{
+		BackupPath:     backupPath,
+		AESKey:         aesKey,
+		AESIv:          aesIv,
+		RestoreDir:     restoreDir,
+		RestoreNginx:   true,
+		RestoreNginxUI: true,
+		VerifyHash:     true,
+	})
+	assert.NoError(t, err)
+	assert.NotEmpty(t, restoreResult.RestoreDir)
+
+	// Verify restored directories
+	nginxUIDir := filepath.Join(restoreDir, NginxUIDir)
+	nginxDir := filepath.Join(restoreDir, NginxDir)
+
+	_, err = os.Stat(nginxUIDir)
+	assert.NoError(t, err)
+	_, err = os.Stat(nginxDir)
+	assert.NoError(t, err)
+
+	// Verify hash info exists
+	_, err = os.Stat(filepath.Join(restoreDir, HashInfoFile))
+	assert.NoError(t, err)
+}
+
+// Test AES encryption/decryption
+func TestEncryptionDecryption(t *testing.T) {
+	// Test data
+	testData := []byte("This is a test message to encrypt and decrypt")
+
+	// Create temp dir for testing
+	testDir, err := os.MkdirTemp("", "nginx-ui-crypto-test-*")
+	assert.NoError(t, err)
+	defer os.RemoveAll(testDir)
+
+	// Create test file
+	testFile := filepath.Join(testDir, "test.txt")
+	err = os.WriteFile(testFile, testData, 0644)
+	assert.NoError(t, err)
+
+	// Generate AES key and IV
+	key, err := GenerateAESKey()
+	assert.NoError(t, err)
+	iv, err := GenerateIV()
+	assert.NoError(t, err)
+
+	// Test encrypt file
+	err = encryptFile(testFile, key, iv)
+	assert.NoError(t, err)
+
+	// Read encrypted data
+	encryptedData, err := os.ReadFile(testFile)
+	assert.NoError(t, err)
+	assert.NotEqual(t, string(testData), string(encryptedData))
+
+	// Test decrypt file
+	err = decryptFile(testFile, key, iv)
+	assert.NoError(t, err)
+
+	// Read decrypted data
+	decryptedData, err := os.ReadFile(testFile)
+	assert.NoError(t, err)
+	assert.Equal(t, string(testData), string(decryptedData))
+}
+
+// Test AES direct encryption/decryption
+func TestAESEncryptDecrypt(t *testing.T) {
+	// Generate key and IV
+	key, err := GenerateAESKey()
+	assert.NoError(t, err)
+
+	iv, err := GenerateIV()
+	assert.NoError(t, err)
+
+	// Test data
+	original := []byte("This is a test message for encryption and decryption")
+
+	// Encrypt
+	encrypted, err := AESEncrypt(original, key, iv)
+	assert.NoError(t, err)
+	assert.NotEqual(t, original, encrypted)
+
+	// Decrypt
+	decrypted, err := AESDecrypt(encrypted, key, iv)
+	assert.NoError(t, err)
+	assert.Equal(t, original, decrypted)
+}
+
+// Test Base64 encoding/decoding
+func TestEncodeDecodeBase64(t *testing.T) {
+	original := []byte("Test data for base64 encoding")
+
+	// Encode
+	encoded := EncodeToBase64(original)
+
+	// Decode
+	decoded, err := DecodeFromBase64(encoded)
+	assert.NoError(t, err)
+	assert.Equal(t, original, decoded)
+}
+
+func TestGenerateAESKey(t *testing.T) {
+	key, err := GenerateAESKey()
+	assert.NoError(t, err)
+	assert.Equal(t, 32, len(key))
+}
+
+func TestGenerateIV(t *testing.T) {
+	iv, err := GenerateIV()
+	assert.NoError(t, err)
+	assert.Equal(t, 16, len(iv))
+}
+
+func TestEncryptDecryptFile(t *testing.T) {
+	// Create temp directory
+	tempDir, err := os.MkdirTemp("", "encrypt-file-test-*")
+	assert.NoError(t, err)
+	defer os.RemoveAll(tempDir)
+
+	// Create test file
+	testFile := filepath.Join(tempDir, "test.txt")
+	testContent := []byte("This is test content for file encryption")
+	err = os.WriteFile(testFile, testContent, 0644)
+	assert.NoError(t, err)
+
+	// Generate key and IV
+	key, err := GenerateAESKey()
+	assert.NoError(t, err)
+
+	iv, err := GenerateIV()
+	assert.NoError(t, err)
+
+	// Encrypt file
+	err = encryptFile(testFile, key, iv)
+	assert.NoError(t, err)
+
+	// Read encrypted content
+	encryptedContent, err := os.ReadFile(testFile)
+	assert.NoError(t, err)
+	assert.NotEqual(t, testContent, encryptedContent)
+
+	// Decrypt file
+	err = decryptFile(testFile, key, iv)
+	assert.NoError(t, err)
+
+	// Read decrypted content
+	decryptedContent, err := os.ReadFile(testFile)
+	assert.NoError(t, err)
+	assert.Equal(t, testContent, decryptedContent)
+}
+
+func TestBackupRestore(t *testing.T) {
+	// Set up test environment
+	tempDir, cleanup := setupTestEnvironment(t)
+	defer cleanup()
+
+	// Create a config.ini file since it's required for the test
+	configDir := filepath.Join(tempDir, "config")
+	configPath := filepath.Join(configDir, "config.ini")
+	err := os.WriteFile(configPath, []byte("[app]\nName = Nginx UI Test\n"), 0644)
+	assert.NoError(t, err)
+
+	// Update Cosy settings path
+	originalConfPath := cosysettings.ConfPath
+	cosysettings.ConfPath = configPath
+	defer func() {
+		cosysettings.ConfPath = originalConfPath
+	}()
+
+	// Create backup
+	backupResult, err := Backup()
+	// If there's an error, log it but continue testing
+	if err != nil {
+		t.Logf("Backup failed with error: %v", err)
+		t.Fail()
+		return
+	}
+
+	assert.NotNil(t, backupResult.BackupContent)
+	assert.NotEmpty(t, backupResult.BackupName)
+	assert.NotEmpty(t, backupResult.AESKey)
+	assert.NotEmpty(t, backupResult.AESIv)
+
+	// Create temporary file for restore testing
+	backupPath := filepath.Join(tempDir, backupResult.BackupName)
+	err = os.WriteFile(backupPath, backupResult.BackupContent, 0644)
+	assert.NoError(t, err)
+
+	// Decode key and IV
+	key, err := DecodeFromBase64(backupResult.AESKey)
+	assert.NoError(t, err)
+
+	iv, err := DecodeFromBase64(backupResult.AESIv)
+	assert.NoError(t, err)
+
+	// Create restore directory
+	restoreDir := filepath.Join(tempDir, "restore")
+	err = os.MkdirAll(restoreDir, 0755)
+	assert.NoError(t, err)
+
+	// Create restore options
+	options := RestoreOptions{
+		BackupPath: backupPath,
+		AESKey:     key,
+		AESIv:      iv,
+		RestoreDir: restoreDir,
+		VerifyHash: true,
+		// Avoid modifying the system
+		RestoreNginx:   false,
+		RestoreNginxUI: false,
+	}
+
+	// Test restore
+	result, err := Restore(options)
+	if err != nil {
+		t.Logf("Restore failed with error: %v", err)
+		t.Fail()
+		return
+	}
+
+	assert.Equal(t, restoreDir, result.RestoreDir)
+	// If hash verification is enabled, check the result
+	if options.VerifyHash {
+		assert.True(t, result.HashMatch, "Hash verification should pass")
+	}
+}
+
+func TestCreateZipArchive(t *testing.T) {
+	// Create temp directories
+	tempSourceDir, err := os.MkdirTemp("", "zip-source-test-*")
+	assert.NoError(t, err)
+	defer os.RemoveAll(tempSourceDir)
+
+	// Create some test files
+	testFiles := []string{"file1.txt", "file2.txt", "subdir/file3.txt"}
+	testContent := []byte("Test content")
+
+	for _, file := range testFiles {
+		filePath := filepath.Join(tempSourceDir, file)
+		dirPath := filepath.Dir(filePath)
+
+		err = os.MkdirAll(dirPath, 0755)
+		assert.NoError(t, err)
+
+		err = os.WriteFile(filePath, testContent, 0644)
+		assert.NoError(t, err)
+	}
+
+	// Create zip file
+	zipPath := filepath.Join(tempSourceDir, "test.zip")
+	err = createZipArchive(zipPath, tempSourceDir)
+	assert.NoError(t, err)
+
+	// Verify zip file was created
+	_, err = os.Stat(zipPath)
+	assert.NoError(t, err)
+
+	// Extract to new directory to verify contents
+	extractDir := filepath.Join(tempSourceDir, "extract")
+	err = os.MkdirAll(extractDir, 0755)
+	assert.NoError(t, err)
+
+	err = extractZipArchive(zipPath, extractDir)
+	assert.NoError(t, err)
+
+	// Verify extracted files
+	for _, file := range testFiles {
+		extractedPath := filepath.Join(extractDir, file)
+		content, err := os.ReadFile(extractedPath)
+		assert.NoError(t, err)
+		assert.Equal(t, testContent, content)
+	}
+}
+
+func TestHashCalculation(t *testing.T) {
+	// Create temp file
+	tempFile, err := os.CreateTemp("", "hash-test-*.txt")
+	assert.NoError(t, err)
+	defer os.Remove(tempFile.Name())
+
+	// Write content
+	testContent := []byte("Test content for hash calculation")
+	_, err = tempFile.Write(testContent)
+	assert.NoError(t, err)
+	tempFile.Close()
+
+	// Calculate hash
+	hash, err := calculateFileHash(tempFile.Name())
+	assert.NoError(t, err)
+	assert.NotEmpty(t, hash)
+
+	// Calculate again to verify consistency
+	hash2, err := calculateFileHash(tempFile.Name())
+	assert.NoError(t, err)
+	assert.Equal(t, hash, hash2)
+
+	// Modify file and check hash changes
+	err = os.WriteFile(tempFile.Name(), []byte("Modified content"), 0644)
+	assert.NoError(t, err)
+
+	hash3, err := calculateFileHash(tempFile.Name())
+	assert.NoError(t, err)
+	assert.NotEqual(t, hash, hash3)
+}

+ 290 - 0
internal/backup/backup_zip.go

@@ -0,0 +1,290 @@
+package backup
+
+import (
+	"archive/zip"
+	"bytes"
+	"crypto/sha256"
+	"encoding/hex"
+	"io"
+	"os"
+	"path/filepath"
+
+	"github.com/uozi-tech/cosy"
+)
+
+// createZipArchive creates a zip archive from a directory
+func createZipArchive(zipPath, srcDir string) error {
+	// Create a new zip file
+	zipFile, err := os.Create(zipPath)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrCreateZipFile, err.Error())
+	}
+	defer zipFile.Close()
+
+	// Create a new zip writer
+	zipWriter := zip.NewWriter(zipFile)
+	defer zipWriter.Close()
+
+	// Walk through all files in the source directory
+	err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		// Get relative path
+		relPath, err := filepath.Rel(srcDir, path)
+		if err != nil {
+			return err
+		}
+
+		// Skip if it's the source directory itself
+		if relPath == "." {
+			return nil
+		}
+
+		// Check if it's a symlink
+		if info.Mode()&os.ModeSymlink != 0 {
+			// Get target of symlink
+			linkTarget, err := os.Readlink(path)
+			if err != nil {
+				return cosy.WrapErrorWithParams(ErrReadSymlink, err.Error())
+			}
+
+			// Create symlink entry in zip
+			header := &zip.FileHeader{
+				Name:   relPath,
+				Method: zip.Deflate,
+			}
+			header.SetMode(info.Mode())
+
+			writer, err := zipWriter.CreateHeader(header)
+			if err != nil {
+				return cosy.WrapErrorWithParams(ErrCreateZipEntry, err.Error())
+			}
+
+			// Write link target as content (common way to store symlinks in zip)
+			_, err = writer.Write([]byte(linkTarget))
+			if err != nil {
+				return cosy.WrapErrorWithParams(ErrCopyContent, relPath)
+			}
+
+			return nil
+		}
+
+		// Create zip header
+		header, err := zip.FileInfoHeader(info)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrCreateZipHeader, err.Error())
+		}
+
+		// Set relative path as name
+		header.Name = relPath
+		if info.IsDir() {
+			header.Name += "/"
+		}
+
+		// Set compression method
+		header.Method = zip.Deflate
+
+		// Create zip entry writer
+		writer, err := zipWriter.CreateHeader(header)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrCreateZipEntry, err.Error())
+		}
+
+		// Skip if it's a directory
+		if info.IsDir() {
+			return nil
+		}
+
+		// Open source file
+		source, err := os.Open(path)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrOpenSourceFile, err.Error())
+		}
+		defer source.Close()
+
+		// Copy to zip
+		_, err = io.Copy(writer, source)
+		return err
+	})
+
+	return err
+}
+
+// createZipArchiveFromFiles creates a zip archive from a list of files
+func createZipArchiveFromFiles(zipPath string, files []string) error {
+	// Create a new zip file
+	zipFile, err := os.Create(zipPath)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrCreateZipFile, err.Error())
+	}
+	defer zipFile.Close()
+
+	// Create a new zip writer
+	zipWriter := zip.NewWriter(zipFile)
+	defer zipWriter.Close()
+
+	// Add each file to the zip
+	for _, file := range files {
+		// Get file info
+		info, err := os.Stat(file)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrOpenSourceFile, err.Error())
+		}
+
+		// Create zip header
+		header, err := zip.FileInfoHeader(info)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrCreateZipHeader, err.Error())
+		}
+
+		// Set base name as header name
+		header.Name = filepath.Base(file)
+
+		// Set compression method
+		header.Method = zip.Deflate
+
+		// Create zip entry writer
+		writer, err := zipWriter.CreateHeader(header)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrCreateZipEntry, err.Error())
+		}
+
+		// Open source file
+		source, err := os.Open(file)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrOpenSourceFile, err.Error())
+		}
+		defer source.Close()
+
+		// Copy to zip
+		_, err = io.Copy(writer, source)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrCopyContent, file)
+		}
+	}
+
+	return nil
+}
+
+// calculateFileHash calculates the SHA-256 hash of a file
+func calculateFileHash(filePath string) (string, error) {
+	// Open file
+	file, err := os.Open(filePath)
+	if err != nil {
+		return "", cosy.WrapErrorWithParams(ErrReadFile, filePath)
+	}
+	defer file.Close()
+
+	// Create hash
+	hash := sha256.New()
+	if _, err := io.Copy(hash, file); err != nil {
+		return "", cosy.WrapErrorWithParams(ErrCalculateHash, err.Error())
+	}
+
+	// Return hex hash
+	return hex.EncodeToString(hash.Sum(nil)), nil
+}
+
+// createZipArchiveToBuffer creates a zip archive of files in the specified directory
+// and writes the zip content to the provided buffer
+func createZipArchiveToBuffer(buffer *bytes.Buffer, sourceDir string) error {
+	// Create a zip writer that writes to the buffer
+	zipWriter := zip.NewWriter(buffer)
+	defer zipWriter.Close()
+
+	// Walk through all files in the source directory
+	err := filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		// Skip the source directory itself
+		if path == sourceDir {
+			return nil
+		}
+
+		// Get the relative path to the source directory
+		relPath, err := filepath.Rel(sourceDir, path)
+		if err != nil {
+			return err
+		}
+
+		// Check if it's a symlink
+		if info.Mode()&os.ModeSymlink != 0 {
+			// Get target of symlink
+			linkTarget, err := os.Readlink(path)
+			if err != nil {
+				return cosy.WrapErrorWithParams(ErrReadSymlink, err.Error())
+			}
+
+			// Create symlink entry in zip
+			header := &zip.FileHeader{
+				Name:   relPath,
+				Method: zip.Deflate,
+			}
+			header.SetMode(info.Mode())
+
+			writer, err := zipWriter.CreateHeader(header)
+			if err != nil {
+				return cosy.WrapErrorWithParams(ErrCreateZipEntry, err.Error())
+			}
+
+			// Write link target as content
+			_, err = writer.Write([]byte(linkTarget))
+			if err != nil {
+				return cosy.WrapErrorWithParams(ErrCopyContent, relPath)
+			}
+
+			return nil
+		}
+
+		// Create a zip header from the file info
+		header, err := zip.FileInfoHeader(info)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrCreateZipHeader, err.Error())
+		}
+
+		// Set the name to be relative to the source directory
+		header.Name = relPath
+
+		// Set the compression method
+		if !info.IsDir() {
+			header.Method = zip.Deflate
+		}
+
+		// Create the entry in the zip file
+		writer, err := zipWriter.CreateHeader(header)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrCreateZipEntry, err.Error())
+		}
+
+		// If it's a directory, we're done
+		if info.IsDir() {
+			return nil
+		}
+
+		// Open the source file
+		file, err := os.Open(path)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrOpenSourceFile, err.Error())
+		}
+		defer file.Close()
+
+		// Copy the file contents to the zip entry
+		_, err = io.Copy(writer, file)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrCopyContent, relPath)
+		}
+
+		return nil
+	})
+
+	if err != nil {
+		return err
+	}
+
+	// Close the zip writer to ensure all data is written
+	return zipWriter.Close()
+}

+ 83 - 0
internal/backup/errors.go

@@ -0,0 +1,83 @@
+package backup
+
+import (
+	"github.com/uozi-tech/cosy"
+)
+
+var (
+	errScope = cosy.NewErrorScope("backup")
+
+	// Backup errors
+	ErrCreateTempDir     = errScope.New(4002, "Failed to create temporary directory")
+	ErrCreateTempSubDir  = errScope.New(4003, "Failed to create temporary subdirectory")
+	ErrBackupNginxUI     = errScope.New(4004, "Failed to backup Nginx UI files: {0}")
+	ErrBackupNginx       = errScope.New(4005, "Failed to backup Nginx config files: {0}")
+	ErrCreateHashFile    = errScope.New(4006, "Failed to create hash info file: {0}")
+	ErrEncryptNginxUIDir = errScope.New(4007, "Failed to encrypt Nginx UI directory: {0}")
+	ErrEncryptNginxDir   = errScope.New(4008, "Failed to encrypt Nginx directory: {0}")
+	ErrCreateZipArchive  = errScope.New(4009, "Failed to create zip archive: {0}")
+	ErrGenerateAESKey    = errScope.New(4011, "Failed to generate AES key: {0}")
+	ErrGenerateIV        = errScope.New(4012, "Failed to generate initialization vector: {0}")
+	ErrCreateBackupFile  = errScope.New(4013, "Failed to create backup file: {0}")
+	ErrCleanupTempDir    = errScope.New(4014, "Failed to cleanup temporary directory: {0}")
+
+	// Config and file errors
+	ErrConfigPathEmpty     = errScope.New(4101, "Config path is empty")
+	ErrCopyConfigFile      = errScope.New(4102, "Failed to copy config file: {0}")
+	ErrCopyDBDir           = errScope.New(4103, "Failed to copy database directory: {0}")
+	ErrCopyDBFile          = errScope.New(4104, "Failed to copy database file: {0}")
+	ErrCalculateHash       = errScope.New(4105, "Failed to calculate hash: {0}")
+	ErrNginxConfigDirEmpty = errScope.New(4106, "Nginx config directory is not set")
+	ErrCopyNginxConfigDir  = errScope.New(4107, "Failed to copy Nginx config directory: {0}")
+	ErrReadSymlink         = errScope.New(4108, "Failed to read symlink: {0}")
+
+	// Encryption and decryption errors
+	ErrReadFile           = errScope.New(4201, "Failed to read file: {0}")
+	ErrEncryptFile        = errScope.New(4202, "Failed to encrypt file: {0}")
+	ErrWriteEncryptedFile = errScope.New(4203, "Failed to write encrypted file: {0}")
+	ErrEncryptData        = errScope.New(4204, "Failed to encrypt data: {0}")
+	ErrDecryptData        = errScope.New(4205, "Failed to decrypt data: {0}")
+	ErrInvalidPadding     = errScope.New(4206, "Invalid padding in decrypted data")
+
+	// Zip file errors
+	ErrCreateZipFile   = errScope.New(4301, "Failed to create zip file: {0}")
+	ErrCreateZipEntry  = errScope.New(4302, "Failed to create zip entry: {0}")
+	ErrOpenSourceFile  = errScope.New(4303, "Failed to open source file: {0}")
+	ErrCreateZipHeader = errScope.New(4304, "Failed to create zip header: {0}")
+	ErrCopyContent     = errScope.New(4305, "Failed to copy file content: {0}")
+	ErrWriteZipBuffer  = errScope.New(4306, "Failed to write to zip buffer: {0}")
+
+	// Restore errors
+	ErrCreateRestoreDir     = errScope.New(4501, "Failed to create restore directory: {0}")
+	ErrExtractArchive       = errScope.New(4505, "Failed to extract archive: {0}")
+	ErrDecryptNginxUIDir    = errScope.New(4506, "Failed to decrypt Nginx UI directory: {0}")
+	ErrDecryptNginxDir      = errScope.New(4507, "Failed to decrypt Nginx directory: {0}")
+	ErrVerifyHashes         = errScope.New(4508, "Failed to verify hashes: {0}")
+	ErrRestoreNginxConfigs  = errScope.New(4509, "Failed to restore Nginx configs: {0}")
+	ErrRestoreNginxUIFiles  = errScope.New(4510, "Failed to restore Nginx UI files: {0}")
+	ErrBackupFileNotFound   = errScope.New(4511, "Backup file not found: {0}")
+	ErrInvalidSecurityToken = errScope.New(4512, "Invalid security token format")
+	ErrInvalidAESKey        = errScope.New(4513, "Invalid AES key format: {0}")
+	ErrInvalidAESIV         = errScope.New(4514, "Invalid AES IV format: {0}")
+
+	// Zip extraction errors
+	ErrOpenZipFile     = errScope.New(4601, "Failed to open zip file: {0}")
+	ErrCreateDir       = errScope.New(4602, "Failed to create directory: {0}")
+	ErrCreateParentDir = errScope.New(4603, "Failed to create parent directory: {0}")
+	ErrCreateFile      = errScope.New(4604, "Failed to create file: {0}")
+	ErrOpenZipEntry    = errScope.New(4605, "Failed to open zip entry: {0}")
+	ErrCreateSymlink   = errScope.New(4606, "Failed to create symbolic link: {0}")
+	ErrInvalidFilePath = errScope.New(4607, "Invalid file path: {0}")
+	ErrEvalSymlinks    = errScope.New(4608, "Failed to evaluate symbolic links: {0}")
+
+	// Decryption errors
+	ErrReadEncryptedFile  = errScope.New(4701, "Failed to read encrypted file: {0}")
+	ErrDecryptFile        = errScope.New(4702, "Failed to decrypt file: {0}")
+	ErrWriteDecryptedFile = errScope.New(4703, "Failed to write decrypted file: {0}")
+
+	// Hash verification errors
+	ErrReadHashFile       = errScope.New(4801, "Failed to read hash info file: {0}")
+	ErrCalculateUIHash    = errScope.New(4802, "Failed to calculate Nginx UI hash: {0}")
+	ErrCalculateNginxHash = errScope.New(4803, "Failed to calculate Nginx hash: {0}")
+	ErrHashMismatch       = errScope.New(4804, "Hash verification failed: file integrity compromised")
+)

+ 440 - 0
internal/backup/restore.go

@@ -0,0 +1,440 @@
+package backup
+
+import (
+	"archive/zip"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/settings"
+	"github.com/uozi-tech/cosy"
+	cosysettings "github.com/uozi-tech/cosy/settings"
+)
+
+// RestoreResult contains the results of a restore operation
+type RestoreResult struct {
+	RestoreDir      string
+	NginxUIRestored bool
+	NginxRestored   bool
+	HashMatch       bool
+}
+
+// RestoreOptions contains options for restore operation
+type RestoreOptions struct {
+	BackupPath     string
+	AESKey         []byte
+	AESIv          []byte
+	RestoreDir     string
+	RestoreNginx   bool
+	VerifyHash     bool
+	RestoreNginxUI bool
+}
+
+// Restore restores data from a backup archive
+func Restore(options RestoreOptions) (RestoreResult, error) {
+	// Create restore directory if it doesn't exist
+	if err := os.MkdirAll(options.RestoreDir, 0755); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrCreateRestoreDir, err.Error())
+	}
+
+	// Extract main archive to restore directory
+	if err := extractZipArchive(options.BackupPath, options.RestoreDir); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, err.Error())
+	}
+
+	// Decrypt the extracted files
+	hashInfoPath := filepath.Join(options.RestoreDir, HashInfoFile)
+	nginxUIZipPath := filepath.Join(options.RestoreDir, NginxUIZipName)
+	nginxZipPath := filepath.Join(options.RestoreDir, NginxZipName)
+
+	// Decrypt hash info file
+	if err := decryptFile(hashInfoPath, options.AESKey, options.AESIv); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrDecryptFile, err.Error())
+	}
+
+	// Decrypt nginx-ui.zip
+	if err := decryptFile(nginxUIZipPath, options.AESKey, options.AESIv); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrDecryptNginxUIDir, err.Error())
+	}
+
+	// Decrypt nginx.zip
+	if err := decryptFile(nginxZipPath, options.AESKey, options.AESIv); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrDecryptNginxDir, err.Error())
+	}
+
+	// Extract zip files to subdirectories
+	nginxUIDir := filepath.Join(options.RestoreDir, NginxUIDir)
+	nginxDir := filepath.Join(options.RestoreDir, NginxDir)
+
+	if err := os.MkdirAll(nginxUIDir, 0755); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrCreateDir, err.Error())
+	}
+
+	if err := os.MkdirAll(nginxDir, 0755); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrCreateDir, err.Error())
+	}
+
+	// Extract nginx-ui.zip to nginx-ui directory
+	if err := extractZipArchive(nginxUIZipPath, nginxUIDir); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, err.Error())
+	}
+
+	// Extract nginx.zip to nginx directory
+	if err := extractZipArchive(nginxZipPath, nginxDir); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, err.Error())
+	}
+
+	result := RestoreResult{
+		RestoreDir:      options.RestoreDir,
+		NginxUIRestored: false,
+		NginxRestored:   false,
+		HashMatch:       false,
+	}
+
+	// Verify hashes if requested
+	if options.VerifyHash {
+		hashMatch, err := verifyHashes(options.RestoreDir, nginxUIZipPath, nginxZipPath)
+		if err != nil {
+			return result, cosy.WrapErrorWithParams(ErrVerifyHashes, err.Error())
+		}
+		result.HashMatch = hashMatch
+	}
+
+	// Restore nginx configs if requested
+	if options.RestoreNginx {
+		if err := restoreNginxConfigs(nginxDir); err != nil {
+			return result, cosy.WrapErrorWithParams(ErrRestoreNginxConfigs, err.Error())
+		}
+		result.NginxRestored = true
+	}
+
+	// Restore nginx-ui config if requested
+	if options.RestoreNginxUI {
+		if err := restoreNginxUIConfig(nginxUIDir); err != nil {
+			return result, cosy.WrapErrorWithParams(ErrBackupNginxUI, err.Error())
+		}
+		result.NginxUIRestored = true
+	}
+
+	return result, nil
+}
+
+// extractZipArchive extracts a zip archive to the specified directory
+func extractZipArchive(zipPath, destDir string) error {
+	reader, err := zip.OpenReader(zipPath)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrOpenZipFile, fmt.Sprintf("failed to open zip file %s: %v", zipPath, err))
+	}
+	defer reader.Close()
+
+	for _, file := range reader.File {
+		err := extractZipFile(file, destDir)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrExtractArchive, fmt.Sprintf("failed to extract file %s: %v", file.Name, err))
+		}
+	}
+
+	return nil
+}
+
+// extractZipFile extracts a single file from a zip archive
+func extractZipFile(file *zip.File, destDir string) error {
+	// Check for directory traversal elements in the file name
+	if strings.Contains(file.Name, "..") {
+		return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("file name contains directory traversal: %s", file.Name))
+	}
+
+	// Clean and normalize the file path
+	cleanName := filepath.Clean(file.Name)
+	if cleanName == "." || cleanName == ".." {
+		return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("invalid file name after cleaning: %s", file.Name))
+	}
+
+	// Create directory path if needed
+	filePath := filepath.Join(destDir, cleanName)
+
+	// Ensure the resulting file path is within the destination directory
+	destDirAbs, err := filepath.Abs(destDir)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("cannot resolve destination path %s: %v", destDir, err))
+	}
+
+	filePathAbs, err := filepath.Abs(filePath)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("cannot resolve file path %s: %v", filePath, err))
+	}
+
+	// Check if the file path is within the destination directory
+	if !strings.HasPrefix(filePathAbs, destDirAbs+string(os.PathSeparator)) {
+		return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("file path %s is outside destination directory %s", filePathAbs, destDirAbs))
+	}
+
+	if file.FileInfo().IsDir() {
+		if err := os.MkdirAll(filePath, file.Mode()); err != nil {
+			return cosy.WrapErrorWithParams(ErrCreateDir, fmt.Sprintf("failed to create directory %s: %v", filePath, err))
+		}
+		return nil
+	}
+
+	// Create parent directory if needed
+	parentDir := filepath.Dir(filePath)
+	if err := os.MkdirAll(parentDir, 0755); err != nil {
+		return cosy.WrapErrorWithParams(ErrCreateParentDir, fmt.Sprintf("failed to create parent directory %s: %v", parentDir, err))
+	}
+
+	// Check if this is a symlink by examining mode bits
+	if file.Mode()&os.ModeSymlink != 0 {
+		// Open source file in zip to read the link target
+		srcFile, err := file.Open()
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrOpenZipEntry, fmt.Sprintf("failed to open symlink source %s: %v", file.Name, err))
+		}
+		defer srcFile.Close()
+
+		// Read the link target
+		linkTargetBytes, err := io.ReadAll(srcFile)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrReadSymlink, fmt.Sprintf("failed to read symlink target for %s: %v", file.Name, err))
+		}
+		linkTarget := string(linkTargetBytes)
+
+		// Clean and normalize the link target
+		cleanLinkTarget := filepath.Clean(linkTarget)
+		if cleanLinkTarget == "." || cleanLinkTarget == ".." {
+			return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("invalid symlink target: %s", linkTarget))
+		}
+
+		// Get allowed paths for symlinks
+		confPath := nginx.GetConfPath()
+		modulesPath := nginx.GetModulesPath()
+
+		// Check if symlink target is to an allowed path (conf path or modules path)
+		isAllowedSymlink := false
+
+		// Check if link points to modules path
+		if filepath.IsAbs(cleanLinkTarget) && (cleanLinkTarget == modulesPath || strings.HasPrefix(cleanLinkTarget, modulesPath+string(filepath.Separator))) {
+			isAllowedSymlink = true
+		}
+
+		// Check if link points to nginx conf path
+		if filepath.IsAbs(cleanLinkTarget) && (cleanLinkTarget == confPath || strings.HasPrefix(cleanLinkTarget, confPath+string(filepath.Separator))) {
+			isAllowedSymlink = true
+		}
+
+		// Handle absolute paths
+		if filepath.IsAbs(cleanLinkTarget) {
+			// Remove any existing file/link at the target path
+			if err := os.RemoveAll(filePath); err != nil && !os.IsNotExist(err) {
+				// Ignoring error, continue creating symlink
+			}
+
+			// If this is a symlink to an allowed path, create it
+			if isAllowedSymlink {
+				if err := os.Symlink(cleanLinkTarget, filePath); err != nil {
+					return cosy.WrapErrorWithParams(ErrCreateSymlink, fmt.Sprintf("failed to create symlink %s -> %s: %v", filePath, cleanLinkTarget, err))
+				}
+				return nil
+			}
+
+			// Otherwise, fallback to creating a directory
+			if err := os.MkdirAll(filePath, 0755); err != nil {
+				return cosy.WrapErrorWithParams(ErrCreateDir, fmt.Sprintf("failed to create directory %s: %v", filePath, err))
+			}
+			return nil
+		}
+
+		// For relative symlinks, verify they don't escape the destination directory
+		absLinkTarget := filepath.Clean(filepath.Join(filepath.Dir(filePath), cleanLinkTarget))
+		if !strings.HasPrefix(absLinkTarget, destDirAbs+string(os.PathSeparator)) {
+			// Create directory instead of symlink if the target is outside destination
+			if err := os.MkdirAll(filePath, 0755); err != nil {
+				return cosy.WrapErrorWithParams(ErrCreateDir, fmt.Sprintf("failed to create directory %s: %v", filePath, err))
+			}
+			return nil
+		}
+
+		// Remove any existing file/link at the target path
+		if err := os.RemoveAll(filePath); err != nil && !os.IsNotExist(err) {
+			// Ignoring error, continue creating symlink
+		}
+
+		// Create the symlink for relative paths within destination
+		if err := os.Symlink(cleanLinkTarget, filePath); err != nil {
+			return cosy.WrapErrorWithParams(ErrCreateSymlink, fmt.Sprintf("failed to create symlink %s -> %s: %v", filePath, cleanLinkTarget, err))
+		}
+
+		// Verify the resolved symlink path is within destination directory
+		resolvedPath, err := filepath.EvalSymlinks(filePath)
+		if err != nil {
+			// If we can't resolve the symlink, it's not a critical error
+			// Just continue
+			return nil
+		}
+
+		resolvedPathAbs, err := filepath.Abs(resolvedPath)
+		if err != nil {
+			// Not a critical error, continue
+			return nil
+		}
+
+		if !strings.HasPrefix(resolvedPathAbs, destDirAbs+string(os.PathSeparator)) {
+			// Remove the symlink if it points outside the destination directory
+			_ = os.Remove(filePath)
+			return cosy.WrapErrorWithParams(ErrInvalidFilePath, fmt.Sprintf("resolved symlink path %s is outside destination directory %s", resolvedPathAbs, destDirAbs))
+		}
+
+		return nil
+	}
+
+	// Create file
+	destFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrCreateFile, fmt.Sprintf("failed to create file %s: %v", filePath, err))
+	}
+	defer destFile.Close()
+
+	// Open source file in zip
+	srcFile, err := file.Open()
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrOpenZipEntry, fmt.Sprintf("failed to open zip entry %s: %v", file.Name, err))
+	}
+	defer srcFile.Close()
+
+	// Copy content
+	if _, err := io.Copy(destFile, srcFile); err != nil {
+		return cosy.WrapErrorWithParams(ErrCopyContent, fmt.Sprintf("failed to copy content for file %s: %v", file.Name, err))
+	}
+
+	return nil
+}
+
+// verifyHashes verifies the hashes of the extracted zip files
+func verifyHashes(restoreDir, nginxUIZipPath, nginxZipPath string) (bool, error) {
+	hashFile := filepath.Join(restoreDir, HashInfoFile)
+	hashContent, err := os.ReadFile(hashFile)
+	if err != nil {
+		return false, cosy.WrapErrorWithParams(ErrReadHashFile, err.Error())
+	}
+
+	hashInfo := parseHashInfo(string(hashContent))
+
+	// Calculate hash for nginx-ui.zip
+	nginxUIHash, err := calculateFileHash(nginxUIZipPath)
+	if err != nil {
+		return false, cosy.WrapErrorWithParams(ErrCalculateUIHash, err.Error())
+	}
+
+	// Calculate hash for nginx.zip
+	nginxHash, err := calculateFileHash(nginxZipPath)
+	if err != nil {
+		return false, cosy.WrapErrorWithParams(ErrCalculateNginxHash, err.Error())
+	}
+
+	// Verify hashes
+	return (hashInfo.NginxUIHash == nginxUIHash && hashInfo.NginxHash == nginxHash), nil
+}
+
+// parseHashInfo parses hash info from content string
+func parseHashInfo(content string) HashInfo {
+	info := HashInfo{}
+	lines := strings.Split(content, "\n")
+
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+
+		parts := strings.SplitN(line, ":", 2)
+		if len(parts) != 2 {
+			continue
+		}
+
+		key := strings.TrimSpace(parts[0])
+		value := strings.TrimSpace(parts[1])
+
+		switch key {
+		case "nginx-ui_hash":
+			info.NginxUIHash = value
+		case "nginx_hash":
+			info.NginxHash = value
+		case "timestamp":
+			info.Timestamp = value
+		case "version":
+			info.Version = value
+		}
+	}
+
+	return info
+}
+
+// restoreNginxConfigs restores nginx configuration files
+func restoreNginxConfigs(nginxBackupDir string) error {
+	destDir := nginx.GetConfPath()
+	if destDir == "" {
+		return ErrNginxConfigDirEmpty
+	}
+
+	// Recursively clean destination directory preserving the directory structure
+	if err := cleanDirectoryPreservingStructure(destDir); err != nil {
+		return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "failed to clean directory: "+err.Error())
+	}
+
+	// Copy files from backup to nginx config directory
+	if err := copyDirectory(nginxBackupDir, destDir); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// cleanDirectoryPreservingStructure removes all files and symlinks in a directory
+// but preserves the directory structure itself
+func cleanDirectoryPreservingStructure(dir string) error {
+	entries, err := os.ReadDir(dir)
+	if err != nil {
+		return err
+	}
+
+	for _, entry := range entries {
+		path := filepath.Join(dir, entry.Name())
+		err = os.RemoveAll(path)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// restoreNginxUIConfig restores nginx-ui configuration files
+func restoreNginxUIConfig(nginxUIBackupDir string) error {
+	// Get config directory
+	configDir := filepath.Dir(cosysettings.ConfPath)
+	if configDir == "" {
+		return ErrConfigPathEmpty
+	}
+
+	// Restore app.ini to the configured location
+	srcConfigPath := filepath.Join(nginxUIBackupDir, "app.ini")
+	if err := copyFile(srcConfigPath, cosysettings.ConfPath); err != nil {
+		return err
+	}
+
+	// Restore database file if exists
+	dbName := settings.DatabaseSettings.GetName()
+	srcDBPath := filepath.Join(nginxUIBackupDir, dbName+".db")
+	destDBPath := filepath.Join(configDir, dbName+".db")
+
+	// Only attempt to copy if database file exists in backup
+	if _, err := os.Stat(srcDBPath); err == nil {
+		if err := copyFile(srcDBPath, destDBPath); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}

+ 85 - 0
internal/backup/utils.go

@@ -0,0 +1,85 @@
+package backup
+
+import (
+	"io"
+	"os"
+	"path/filepath"
+
+	"github.com/uozi-tech/cosy"
+)
+
+// copyFile copies a file from src to dst
+func copyFile(src, dst string) error {
+	// Open source file
+	source, err := os.Open(src)
+	if err != nil {
+		return err
+	}
+	defer source.Close()
+
+	// Create destination file
+	destination, err := os.Create(dst)
+	if err != nil {
+		return err
+	}
+	defer destination.Close()
+
+	// Copy content
+	_, err = io.Copy(destination, source)
+	return err
+}
+
+// copyDirectory copies a directory recursively from src to dst
+func copyDirectory(src, dst string) error {
+	// Check if source is a directory
+	srcInfo, err := os.Stat(src)
+	if err != nil {
+		return err
+	}
+	if !srcInfo.IsDir() {
+		return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "%s is not a directory", src)
+	}
+
+	// Create destination directory
+	if err := os.MkdirAll(dst, srcInfo.Mode()); err != nil {
+		return err
+	}
+
+	// Walk through source directory
+	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+
+		// Calculate relative path
+		relPath, err := filepath.Rel(src, path)
+		if err != nil {
+			return err
+		}
+		if relPath == "." {
+			return nil
+		}
+
+		// Create target path
+		targetPath := filepath.Join(dst, relPath)
+
+		// Check if it's a symlink
+		if info.Mode()&os.ModeSymlink != 0 {
+			// Read the link
+			linkTarget, err := os.Readlink(path)
+			if err != nil {
+				return err
+			}
+			// Create symlink at target path
+			return os.Symlink(linkTarget, targetPath)
+		}
+
+		// If it's a directory, create it
+		if info.IsDir() {
+			return os.MkdirAll(targetPath, info.Mode())
+		}
+
+		// If it's a file, copy it
+		return copyFile(path, targetPath)
+	})
+}

+ 117 - 0
internal/backup/version_test.go

@@ -0,0 +1,117 @@
+package backup
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/0xJacky/Nginx-UI/internal/version"
+	"github.com/0xJacky/Nginx-UI/settings"
+	"github.com/stretchr/testify/assert"
+	cosysettings "github.com/uozi-tech/cosy/settings"
+)
+
+// TestBackupVersion verifies that the backup file contains correct version information
+func TestBackupVersion(t *testing.T) {
+	// Make sure backup files are cleaned up at the start and end of the test
+	cleanupBackupFiles()
+	defer cleanupBackupFiles()
+
+	// Create test configuration
+	tempDir, err := os.MkdirTemp("", "nginx-ui-backup-version-test-*")
+	assert.NoError(t, err)
+	defer os.RemoveAll(tempDir)
+
+	// Create config file
+	configPath := filepath.Join(tempDir, "config.ini")
+	testConfig := []byte("[app]\nName = Nginx UI Test\n")
+	err = os.WriteFile(configPath, testConfig, 0644)
+	assert.NoError(t, err)
+
+	// Create database file
+	dbName := settings.DatabaseSettings.GetName()
+	dbFile := dbName + ".db"
+	dbPath := filepath.Join(tempDir, dbFile)
+	testDB := []byte("CREATE TABLE users (id INT, name TEXT);")
+	err = os.WriteFile(dbPath, testDB, 0644)
+	assert.NoError(t, err)
+
+	// Create nginx directory
+	nginxConfigDir := filepath.Join(tempDir, "nginx")
+	err = os.MkdirAll(nginxConfigDir, 0755)
+	assert.NoError(t, err)
+
+	// Create nginx config
+	testNginxContent := []byte("server {\n    listen 80;\n    server_name example.com;\n}\n")
+	err = os.WriteFile(filepath.Join(nginxConfigDir, "nginx.conf"), testNginxContent, 0644)
+	assert.NoError(t, err)
+
+	// Setup test environment
+	originalConfPath := cosysettings.ConfPath
+	originalNginxConfigDir := settings.NginxSettings.ConfigDir
+
+	cosysettings.ConfPath = configPath
+	settings.NginxSettings.ConfigDir = nginxConfigDir
+
+	// Restore original settings after test
+	defer func() {
+		cosysettings.ConfPath = originalConfPath
+		settings.NginxSettings.ConfigDir = originalNginxConfigDir
+	}()
+
+	// Run backup
+	result, err := Backup()
+	assert.NoError(t, err)
+	assert.NotEmpty(t, result.BackupContent)
+	assert.NotEmpty(t, result.BackupName)
+	assert.NotEmpty(t, result.AESKey)
+	assert.NotEmpty(t, result.AESIv)
+
+	// Save backup content to temporary file for restore testing
+	backupFile := filepath.Join(tempDir, result.BackupName)
+	err = os.WriteFile(backupFile, result.BackupContent, 0644)
+	assert.NoError(t, err)
+
+	// Decode AES key and IV
+	key, err := DecodeFromBase64(result.AESKey)
+	assert.NoError(t, err)
+	iv, err := DecodeFromBase64(result.AESIv)
+	assert.NoError(t, err)
+
+	// Use the Restore function to extract and verify
+	restoreDir, err := os.MkdirTemp("", "nginx-ui-restore-version-test-*")
+	assert.NoError(t, err)
+	defer os.RemoveAll(restoreDir)
+
+	restoreResult, err := Restore(RestoreOptions{
+		BackupPath:     backupFile,
+		AESKey:         key,
+		AESIv:          iv,
+		RestoreDir:     restoreDir,
+		VerifyHash:     true,
+		RestoreNginx:   false,
+		RestoreNginxUI: false,
+	})
+	assert.NoError(t, err)
+	assert.True(t, restoreResult.HashMatch, "Hash should match")
+
+	// Check hash_info.txt file
+	hashInfoPath := filepath.Join(restoreDir, HashInfoFile)
+	hashInfoContent, err := os.ReadFile(hashInfoPath)
+	assert.NoError(t, err)
+
+	// Verify version information
+	versionInfo := version.GetVersionInfo()
+	expectedVersion := versionInfo.Version
+
+	// Check if hash_info.txt contains version info
+	hashInfoStr := string(hashInfoContent)
+	t.Logf("Hash info content: %s", hashInfoStr)
+
+	assert.True(t, strings.Contains(hashInfoStr, "version: "), "Hash info should contain version field")
+
+	// Parse hash_info.txt content
+	info := parseHashInfo(hashInfoStr)
+	assert.Equal(t, expectedVersion, info.Version, "Backup version should match current version")
+}

+ 82 - 0
internal/middleware/encrypted_params.go

@@ -4,7 +4,10 @@ import (
 	"bytes"
 	"encoding/json"
 	"io"
+	"mime/multipart"
 	"net/http"
+	"net/url"
+	"strings"
 
 	"github.com/0xJacky/Nginx-UI/internal/crypto"
 	"github.com/gin-gonic/gin"
@@ -15,6 +18,7 @@ var (
 	e                       = cosy.NewErrorScope("middleware")
 	ErrInvalidRequestFormat = e.New(40000, "invalid request format")
 	ErrDecryptionFailed     = e.New(40001, "decryption failed")
+	ErrFormParseFailed      = e.New(40002, "form parse failed")
 )
 
 func EncryptedParams() gin.HandlerFunc {
@@ -44,3 +48,81 @@ func EncryptedParams() gin.HandlerFunc {
 		c.Next()
 	}
 }
+
+// EncryptedForm handles multipart/form-data with encrypted fields while preserving file uploads
+func EncryptedForm() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		// Only process if the content type is multipart/form-data
+		if !strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
+			c.Next()
+			return
+		}
+
+		// Parse the multipart form
+		if err := c.Request.ParseMultipartForm(512 << 20); err != nil { // 512MB max memory
+			c.AbortWithStatusJSON(http.StatusBadRequest, ErrFormParseFailed)
+			return
+		}
+
+		// Check if encrypted_params field exists
+		encryptedParams := c.Request.FormValue("encrypted_params")
+		if encryptedParams == "" {
+			// No encryption, continue normally
+			c.Next()
+			return
+		}
+
+		// Decrypt the parameters
+		params, err := crypto.Decrypt(encryptedParams)
+		if err != nil {
+			c.AbortWithStatusJSON(http.StatusBadRequest, ErrDecryptionFailed)
+			return
+		}
+
+		// Create a new multipart form with the decrypted data
+		newForm := &multipart.Form{
+			Value: make(map[string][]string),
+			File:  c.Request.MultipartForm.File, // Keep original file uploads
+		}
+
+		// Add decrypted values to the new form
+		for key, val := range params {
+			strVal, ok := val.(string)
+			if ok {
+				newForm.Value[key] = []string{strVal}
+			} else {
+				// Handle other types if necessary
+				jsonVal, _ := json.Marshal(val)
+				newForm.Value[key] = []string{string(jsonVal)}
+			}
+		}
+
+		// Also copy original non-encrypted form values (except encrypted_params)
+		for key, vals := range c.Request.MultipartForm.Value {
+			if key != "encrypted_params" && newForm.Value[key] == nil {
+				newForm.Value[key] = vals
+			}
+		}
+
+		// Replace the original form with our modified one
+		c.Request.MultipartForm = newForm
+
+		// Remove the encrypted_params field from the form
+		delete(c.Request.MultipartForm.Value, "encrypted_params")
+
+		// Reset ContentLength as form structure has changed
+		c.Request.ContentLength = -1
+
+		// Sync the form values to the request PostForm to ensure Gin can access them
+		if c.Request.PostForm == nil {
+			c.Request.PostForm = make(url.Values)
+		}
+
+		// Copy all values from MultipartForm to PostForm
+		for k, v := range newForm.Value {
+			c.Request.PostForm[k] = v
+		}
+
+		c.Next()
+	}
+}

+ 25 - 1
internal/nginx/nginx.go

@@ -1,10 +1,12 @@
 package nginx
 
 import (
-	"github.com/0xJacky/Nginx-UI/settings"
 	"os/exec"
+	"strings"
 	"sync"
 	"time"
+
+	"github.com/0xJacky/Nginx-UI/settings"
 )
 
 var (
@@ -74,6 +76,28 @@ func GetLastOutput() string {
 	return lastOutput
 }
 
+// GetModulesPath returns the nginx modules path
+func GetModulesPath() string {
+	// First try to get from nginx -V output
+	output := execCommand("nginx", "-V")
+	if output != "" {
+		// Look for --modules-path in the output
+		if strings.Contains(output, "--modules-path=") {
+			parts := strings.Split(output, "--modules-path=")
+			if len(parts) > 1 {
+				// Extract the path
+				path := strings.Split(parts[1], " ")[0]
+				// Remove quotes if present
+				path = strings.Trim(path, "\"")
+				return path
+			}
+		}
+	}
+
+	// Default path if not found
+	return "/usr/lib/nginx/modules"
+}
+
 func execShell(cmd string) (out string) {
 	bytes, err := exec.Command("/bin/sh", "-c", cmd).CombinedOutput()
 	out = string(bytes)

+ 10 - 0
internal/system/errors.go

@@ -0,0 +1,10 @@
+package system
+
+import "github.com/uozi-tech/cosy"
+
+// System error definitions
+var (
+	e                 = cosy.NewErrorScope("system")
+	ErrInstalled      = e.New(40301, "Nginx UI already installed")
+	ErrInstallTimeout = e.New(40302, "installation is not allowed after 10 minutes of system startup")
+)

+ 8 - 4
query/notifications.gen.go

@@ -34,7 +34,8 @@ func newNotification(db *gorm.DB, opts ...gen.DOOption) notification {
 	_notification.DeletedAt = field.NewField(tableName, "deleted_at")
 	_notification.Type = field.NewInt(tableName, "type")
 	_notification.Title = field.NewString(tableName, "title")
-	_notification.Details = field.NewString(tableName, "details")
+	_notification.Content = field.NewString(tableName, "content")
+	_notification.Details = field.NewField(tableName, "details")
 
 	_notification.fillFieldMap()
 
@@ -51,7 +52,8 @@ type notification struct {
 	DeletedAt field.Field
 	Type      field.Int
 	Title     field.String
-	Details   field.String
+	Content   field.String
+	Details   field.Field
 
 	fieldMap map[string]field.Expr
 }
@@ -74,7 +76,8 @@ func (n *notification) updateTableName(table string) *notification {
 	n.DeletedAt = field.NewField(table, "deleted_at")
 	n.Type = field.NewInt(table, "type")
 	n.Title = field.NewString(table, "title")
-	n.Details = field.NewString(table, "details")
+	n.Content = field.NewString(table, "content")
+	n.Details = field.NewField(table, "details")
 
 	n.fillFieldMap()
 
@@ -91,13 +94,14 @@ func (n *notification) GetFieldByName(fieldName string) (field.OrderExpr, bool)
 }
 
 func (n *notification) fillFieldMap() {
-	n.fieldMap = make(map[string]field.Expr, 7)
+	n.fieldMap = make(map[string]field.Expr, 8)
 	n.fieldMap["id"] = n.ID
 	n.fieldMap["created_at"] = n.CreatedAt
 	n.fieldMap["updated_at"] = n.UpdatedAt
 	n.fieldMap["deleted_at"] = n.DeletedAt
 	n.fieldMap["type"] = n.Type
 	n.fieldMap["title"] = n.Title
+	n.fieldMap["content"] = n.Content
 	n.fieldMap["details"] = n.Details
 }
 

+ 5 - 1
query/site_categories.gen.go

@@ -34,6 +34,7 @@ func newSiteCategory(db *gorm.DB, opts ...gen.DOOption) siteCategory {
 	_siteCategory.DeletedAt = field.NewField(tableName, "deleted_at")
 	_siteCategory.Name = field.NewString(tableName, "name")
 	_siteCategory.SyncNodeIds = field.NewField(tableName, "sync_node_ids")
+	_siteCategory.OrderID = field.NewInt(tableName, "order_id")
 
 	_siteCategory.fillFieldMap()
 
@@ -50,6 +51,7 @@ type siteCategory struct {
 	DeletedAt   field.Field
 	Name        field.String
 	SyncNodeIds field.Field
+	OrderID     field.Int
 
 	fieldMap map[string]field.Expr
 }
@@ -72,6 +74,7 @@ func (s *siteCategory) updateTableName(table string) *siteCategory {
 	s.DeletedAt = field.NewField(table, "deleted_at")
 	s.Name = field.NewString(table, "name")
 	s.SyncNodeIds = field.NewField(table, "sync_node_ids")
+	s.OrderID = field.NewInt(table, "order_id")
 
 	s.fillFieldMap()
 
@@ -88,13 +91,14 @@ func (s *siteCategory) GetFieldByName(fieldName string) (field.OrderExpr, bool)
 }
 
 func (s *siteCategory) fillFieldMap() {
-	s.fieldMap = make(map[string]field.Expr, 6)
+	s.fieldMap = make(map[string]field.Expr, 7)
 	s.fieldMap["id"] = s.ID
 	s.fieldMap["created_at"] = s.CreatedAt
 	s.fieldMap["updated_at"] = s.UpdatedAt
 	s.fieldMap["deleted_at"] = s.DeletedAt
 	s.fieldMap["name"] = s.Name
 	s.fieldMap["sync_node_ids"] = s.SyncNodeIds
+	s.fieldMap["order_id"] = s.OrderID
 }
 
 func (s siteCategory) clone(db *gorm.DB) siteCategory {

+ 2 - 1
router/routers.go

@@ -9,13 +9,13 @@ import (
 	"github.com/0xJacky/Nginx-UI/api/certificate"
 	"github.com/0xJacky/Nginx-UI/api/cluster"
 	"github.com/0xJacky/Nginx-UI/api/config"
+	"github.com/0xJacky/Nginx-UI/api/crypto"
 	"github.com/0xJacky/Nginx-UI/api/nginx"
 	nginxLog "github.com/0xJacky/Nginx-UI/api/nginx_log"
 	"github.com/0xJacky/Nginx-UI/api/notification"
 	"github.com/0xJacky/Nginx-UI/api/openai"
 	"github.com/0xJacky/Nginx-UI/api/public"
 	"github.com/0xJacky/Nginx-UI/api/settings"
-	"github.com/0xJacky/Nginx-UI/api/crypto"
 	"github.com/0xJacky/Nginx-UI/api/sites"
 	"github.com/0xJacky/Nginx-UI/api/streams"
 	"github.com/0xJacky/Nginx-UI/api/system"
@@ -45,6 +45,7 @@ func InitRouter() {
 		public.InitRouter(root)
 		crypto.InitPublicRouter(root)
 		system.InitPublicRouter(root)
+		system.InitBackupRestoreRouter(root)
 		user.InitAuthRouter(root)
 
 		// Authorization required and not websocket request

+ 5 - 4
settings/settings.go

@@ -1,14 +1,15 @@
 package settings
 
 import (
-	"github.com/caarlos0/env/v11"
-	"github.com/elliotchance/orderedmap/v3"
-	"github.com/spf13/cast"
-	"github.com/uozi-tech/cosy/settings"
 	"log"
 	"os"
 	"strings"
 	"time"
+
+	"github.com/caarlos0/env/v11"
+	"github.com/elliotchance/orderedmap/v3"
+	"github.com/spf13/cast"
+	"github.com/uozi-tech/cosy/settings"
 )
 
 var (

部分文件因为文件数量过多而无法显示