Jacky 1 місяць тому
батько
коміт
4cb4695e7b
52 змінених файлів з 8835 додано та 1255 видалено
  1. 161 0
      api/system/backup.go
  2. 390 0
      api/system/backup_test.go
  3. 4 0
      api/system/router.go
  4. 1 0
      app/components.d.ts
  5. 56 0
      app/src/api/backup.ts
  6. 55 0
      app/src/constants/errors/backup.ts
  7. 454 50
      app/src/language/ar/app.po
  8. 448 50
      app/src/language/de_DE/app.po
  9. 446 50
      app/src/language/en/app.po
  10. 448 50
      app/src/language/es/app.po
  11. 450 50
      app/src/language/fr_FR/app.po
  12. 444 50
      app/src/language/ko_KR/app.po
  13. 392 52
      app/src/language/messages.pot
  14. 425 100
      app/src/language/ru_RU/app.po
  15. 448 50
      app/src/language/tr_TR/app.po
  16. 444 50
      app/src/language/vi_VN/app.po
  17. 419 55
      app/src/language/zh_CN/app.po
  18. 455 50
      app/src/language/zh_TW/app.po
  19. 34 5
      app/src/lib/http/index.ts
  20. 37 310
      app/src/routes/index.ts
  21. 16 0
      app/src/routes/modules/auth.ts
  22. 61 0
      app/src/routes/modules/certificates.ts
  23. 35 0
      app/src/routes/modules/config.ts
  24. 14 0
      app/src/routes/modules/dashboard.ts
  25. 20 0
      app/src/routes/modules/environments.ts
  26. 10 0
      app/src/routes/modules/error.ts
  27. 36 0
      app/src/routes/modules/nginx_log.ts
  28. 14 0
      app/src/routes/modules/notifications.ts
  29. 14 0
      app/src/routes/modules/preference.ts
  30. 47 0
      app/src/routes/modules/sites.ts
  31. 24 0
      app/src/routes/modules/streams.ts
  32. 49 0
      app/src/routes/modules/system.ts
  33. 14 0
      app/src/routes/modules/terminal.ts
  34. 14 0
      app/src/routes/modules/user.ts
  35. 1 1
      app/src/version.json
  36. 245 0
      app/src/views/system/Backup/BackupCreator.vue
  37. 192 0
      app/src/views/system/Backup/SystemRestore.vue
  38. 11 0
      app/src/views/system/Backup/index.vue
  39. 66 70
      go.mod
  40. 140 203
      go.sum
  41. 169 0
      internal/backup/backup.go
  42. 128 0
      internal/backup/backup_crypto.go
  43. 76 0
      internal/backup/backup_nginx_ui.go
  44. 466 0
      internal/backup/backup_test.go
  45. 290 0
      internal/backup/backup_zip.go
  46. 83 0
      internal/backup/errors.go
  47. 369 0
      internal/backup/restore.go
  48. 85 0
      internal/backup/utils.go
  49. 117 0
      internal/backup/version_test.go
  50. 8 4
      query/notifications.gen.go
  51. 5 1
      query/site_categories.gen.go
  52. 5 4
      settings/settings.go

+ 161 - 0
api/system/backup.go

@@ -0,0 +1,161 @@
+package system
+
+import (
+	"bytes"
+	"encoding/base64"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+	"time"
+
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/backup"
+	"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"`
+}
+
+// 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 {
+		api.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)
+}
+
+// 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 {
+		api.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrBackupFileNotFound, err.Error()))
+		return
+	}
+
+	// Validate security token
+	if securityToken == "" {
+		api.ErrHandler(c, backup.ErrInvalidSecurityToken)
+		return
+	}
+
+	// Split security token to get Key and IV
+	parts := strings.Split(securityToken, ":")
+	if len(parts) != 2 {
+		api.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 {
+		api.ErrHandler(c, cosy.WrapErrorWithParams(backup.ErrInvalidAESKey, err.Error()))
+		return
+	}
+
+	iv, err := base64.StdEncoding.DecodeString(aesIv)
+	if err != nil {
+		api.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 {
+		api.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 {
+		api.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 {
+		api.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)
+		api.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 restoreNginxUI {
+		go func() {
+			time.Sleep(3 * time.Second)
+			// gracefully restart
+			overseer.Restart()
+		}()
+	}
+
+	c.JSON(http.StatusOK, RestoreResponse{
+		NginxUIRestored: result.NginxUIRestored,
+		NginxRestored:   result.NginxRestored,
+		HashMatch:       result.HashMatch,
+	})
+}

+ 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)
+}

+ 4 - 0
api/system/router.go

@@ -16,6 +16,10 @@ func InitPrivateRouter(r *gin.RouterGroup) {
 	r.GET("upgrade/current", GetCurrentVersion)
 	r.GET("self_check", SelfCheck)
 	r.POST("self_check/:name/fix", SelfCheckFix)
+
+	// Backup and restore endpoints
+	r.GET("system/backup", CreateBackup)
+	r.POST("system/backup/restore", RestoreBackup)
 }
 
 func InitWebSocketRouter(r *gin.RouterGroup) {

+ 1 - 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']

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

@@ -0,0 +1,56 @@
+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',
+      },
+    })
+  },
+}
+
+export default backup

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

@@ -0,0 +1,55 @@
+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}'),
+  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}'),
+  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}'),
+  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'),
+}

Різницю між файлами не показано, бо вона завелика
+ 454 - 50
app/src/language/ar/app.po


Різницю між файлами не показано, бо вона завелика
+ 448 - 50
app/src/language/de_DE/app.po


Різницю між файлами не показано, бо вона завелика
+ 446 - 50
app/src/language/en/app.po


Різницю між файлами не показано, бо вона завелика
+ 448 - 50
app/src/language/es/app.po


Різницю між файлами не показано, бо вона завелика
+ 450 - 50
app/src/language/fr_FR/app.po


Різницю між файлами не показано, бо вона завелика
+ 444 - 50
app/src/language/ko_KR/app.po


+ 392 - 52
app/src/language/messages.pot

@@ -10,16 +10,16 @@ msgstr ""
 msgid "2FA Settings"
 msgstr ""
 
-#: src/routes/index.ts:290
+#: src/routes/modules/system.ts:26
 msgid "About"
 msgstr ""
 
-#: src/routes/index.ts:210
+#: src/routes/modules/nginx_log.ts:17
 #: src/views/site/ngx_conf/LogEntry.vue:75
 msgid "Access Logs"
 msgstr ""
 
-#: src/routes/index.ts:148
+#: src/routes/modules/certificates.ts:20
 #: src/views/certificate/ACMEUser.vue:113
 #: src/views/certificate/ACMEUserSelector.vue:85
 msgid "ACME User"
@@ -32,7 +32,7 @@ msgstr ""
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
 #: src/views/preference/AuthSettings.vue:30
-#: src/views/site/site_category/columns.ts:29
+#: src/views/site/site_category/columns.ts:28
 #: src/views/site/site_list/columns.tsx:76
 #: src/views/stream/StreamList.vue:49
 #: src/views/user/userColumns.tsx:60
@@ -54,7 +54,7 @@ msgstr ""
 msgid "Add a passkey"
 msgstr ""
 
-#: src/routes/index.ts:118
+#: src/routes/modules/config.ts:20
 #: src/views/config/ConfigEditor.vue:144
 #: src/views/config/ConfigEditor.vue:204
 msgid "Add Configuration"
@@ -69,8 +69,8 @@ msgstr ""
 msgid "Add Location"
 msgstr ""
 
-#: src/routes/index.ts:63
-#: src/views/site/SiteAdd.vue:89
+#: src/routes/modules/sites.ts:26
+#: src/views/site/site_add/SiteAdd.vue:89
 msgid "Add Site"
 msgstr ""
 
@@ -240,6 +240,10 @@ msgstr ""
 msgid "Auto-renewal enabled for %{name}"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:92
+msgid "Automatic Restart"
+msgstr ""
+
 #: src/views/certificate/CertificateEditor.vue:255
 #: src/views/config/ConfigEditor.vue:213
 #: src/views/config/ConfigList.vue:106
@@ -258,6 +262,22 @@ msgstr ""
 msgid "Back to list"
 msgstr ""
 
+#: src/routes/modules/system.ts:33
+msgid "Backup"
+msgstr ""
+
+#: src/views/system/Backup/SystemRestore.vue:100
+msgid "Backup file integrity check failed, it may have been tampered with"
+msgstr ""
+
+#: src/constants/errors/backup.ts:39
+msgid "Backup file not found: {0}"
+msgstr ""
+
+#: src/views/system/Backup/BackupCreator.vue:42
+msgid "Backup has been downloaded successfully"
+msgstr ""
+
 #: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr ""
@@ -270,7 +290,7 @@ msgstr ""
 msgid "Banned Until"
 msgstr ""
 
-#: src/views/site/SiteAdd.vue:95
+#: src/views/site/site_add/SiteAdd.vue:95
 msgid "Base information"
 msgstr ""
 
@@ -377,12 +397,12 @@ msgid_plural "Certificates Status"
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/routes/index.ts:139
+#: src/routes/modules/certificates.ts:11
 #: src/views/certificate/CertificateList/Certificate.vue:13
 msgid "Certificates"
 msgstr ""
 
-#: src/routes/index.ts:156
+#: src/routes/modules/certificates.ts:28
 msgid "Certificates List"
 msgstr ""
 
@@ -449,6 +469,10 @@ msgstr ""
 msgid "Cleared successfully"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:137
+msgid "Click or drag backup file to this area to upload"
+msgstr ""
+
 #: src/views/preference/components/TOTP.vue:110
 msgid "Click to copy"
 msgstr ""
@@ -464,6 +488,10 @@ msgstr ""
 msgid "Comments"
 msgstr ""
 
+#: src/constants/errors/backup.ts:13
+msgid "Config path is empty"
+msgstr ""
+
 #: src/views/site/ngx_conf/config_template/ConfigTemplate.vue:84
 msgid "Config Templates"
 msgstr ""
@@ -472,7 +500,7 @@ msgstr ""
 msgid "Configuration file is test successful"
 msgstr ""
 
-#: src/views/site/SiteAdd.vue:101
+#: src/views/site/site_add/SiteAdd.vue:101
 msgid "Configuration Name"
 msgstr ""
 
@@ -480,7 +508,7 @@ msgstr ""
 msgid "Configurations"
 msgstr ""
 
-#: src/views/site/SiteAdd.vue:96
+#: src/views/site/site_add/SiteAdd.vue:96
 msgid "Configure SSL"
 msgstr ""
 
@@ -504,7 +532,12 @@ msgstr ""
 msgid "Copied"
 msgstr ""
 
+#: src/views/system/Backup/BackupCreator.vue:128
+msgid "Copied!"
+msgstr ""
+
 #: src/components/SensitiveString/SensitiveString.vue:37
+#: src/views/system/Backup/BackupCreator.vue:128
 msgid "Copy"
 msgstr ""
 
@@ -528,10 +561,14 @@ msgstr ""
 msgid "Create"
 msgstr ""
 
-#: src/views/site/SiteAdd.vue:157
+#: src/views/site/site_add/SiteAdd.vue:157
 msgid "Create Another"
 msgstr ""
 
+#: src/views/system/Backup/BackupCreator.vue:86
+msgid "Create Backup"
+msgstr ""
+
 #: src/views/config/ConfigList.vue:116
 msgid "Create File"
 msgstr ""
@@ -541,9 +578,13 @@ msgstr ""
 msgid "Create Folder"
 msgstr ""
 
+#: src/views/system/Backup/BackupCreator.vue:75
+msgid "Create system backups including Nginx configuration and Nginx UI settings. Backup files will be automatically downloaded to your computer."
+msgstr ""
+
 #: src/views/notification/notificationColumns.tsx:58
 #: src/views/preference/components/Passkey.vue:95
-#: src/views/site/site_category/columns.ts:15
+#: src/views/site/site_category/columns.ts:16
 #: src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgstr ""
@@ -585,7 +626,7 @@ msgstr ""
 msgid "Customize the name of local node to be displayed in the environment indicator."
 msgstr ""
 
-#: src/routes/index.ts:38
+#: src/routes/modules/dashboard.ts:10
 #: src/views/config/ConfigEditor.vue:134
 #: src/views/config/ConfigEditor.vue:97
 #: src/views/config/ConfigList.vue:64
@@ -771,7 +812,7 @@ msgstr ""
 msgid "Disk IO"
 msgstr ""
 
-#: src/routes/index.ts:184
+#: src/routes/modules/certificates.ts:56
 #: src/views/certificate/DNSCredential.vue:40
 msgid "DNS Credentials"
 msgstr ""
@@ -874,16 +915,16 @@ msgstr ""
 msgid "Edit %{n}"
 msgstr ""
 
-#: src/routes/index.ts:128
+#: src/routes/modules/config.ts:30
 #: src/views/config/ConfigEditor.vue:204
 msgid "Edit Configuration"
 msgstr ""
 
-#: src/routes/index.ts:78
+#: src/routes/modules/sites.ts:41
 msgid "Edit Site"
 msgstr ""
 
-#: src/routes/index.ts:98
+#: src/routes/modules/streams.ts:19
 msgid "Edit Stream"
 msgstr ""
 
@@ -908,7 +949,7 @@ msgstr ""
 msgid "Enable auto-renewal failed for %{name}"
 msgstr ""
 
-#: src/views/site/SiteAdd.vue:43
+#: src/views/site/site_add/SiteAdd.vue:43
 msgid "Enable failed"
 msgstr ""
 
@@ -968,9 +1009,9 @@ msgstr ""
 msgid "Enabled"
 msgstr ""
 
+#: src/views/site/site_add/SiteAdd.vue:40
 #: src/views/site/site_edit/RightSettings.vue:33
 #: src/views/site/site_list/SiteList.vue:46
-#: src/views/site/SiteAdd.vue:40
 #: src/views/stream/components/RightSettings.vue:29
 #: src/views/stream/StreamList.vue:61
 msgid "Enabled successfully"
@@ -984,7 +1025,7 @@ msgstr ""
 msgid "Environment variables cleaned"
 msgstr ""
 
-#: src/routes/index.ts:234
+#: src/routes/modules/environments.ts:11
 #: src/views/dashboard/Environments.vue:83
 #: src/views/environment/Environment.vue:43
 msgid "Environments"
@@ -996,7 +1037,7 @@ msgstr ""
 msgid "Error"
 msgstr ""
 
-#: src/routes/index.ts:217
+#: src/routes/modules/nginx_log.ts:24
 #: src/views/site/ngx_conf/LogEntry.vue:83
 msgid "Error Logs"
 msgstr ""
@@ -1022,10 +1063,114 @@ msgstr ""
 msgid "Fail to obtain certificate"
 msgstr ""
 
+#: src/constants/errors/backup.ts:5
+msgid "Failed to backup Nginx config files: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:4
+msgid "Failed to backup Nginx UI files: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:17
+msgid "Failed to calculate hash: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:53
+msgid "Failed to calculate Nginx hash: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:52
+msgid "Failed to calculate Nginx UI hash: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:14
+msgid "Failed to copy config file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:15
+msgid "Failed to copy database directory: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:16
+msgid "Failed to copy database file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:30
+msgid "Failed to copy file content: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:19
+msgid "Failed to copy Nginx config directory: {0}"
+msgstr ""
+
 #: src/constants/errors/self_check.ts:9
 msgid "Failed to create backup"
 msgstr ""
 
+#: src/constants/errors/backup.ts:12
+msgid "Failed to create backup file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:44
+msgid "Failed to create directory: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:46
+msgid "Failed to create file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:6
+msgid "Failed to create hash info file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:45
+msgid "Failed to create parent directory: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:32
+msgid "Failed to create restore directory: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:2
+msgid "Failed to create temporary directory"
+msgstr ""
+
+#: src/constants/errors/backup.ts:3
+msgid "Failed to create temporary subdirectory"
+msgstr ""
+
+#: src/constants/errors/backup.ts:9
+msgid "Failed to create zip archive: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:27
+msgid "Failed to create zip entry: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:26
+msgid "Failed to create zip file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:29
+msgid "Failed to create zip header: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:24
+msgid "Failed to decrypt data: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:49
+msgid "Failed to decrypt file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:35
+msgid "Failed to decrypt Nginx directory: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:34
+msgid "Failed to decrypt Nginx UI directory: {0}"
+msgstr ""
+
 #: src/views/site/site_edit/RightSettings.vue:45
 #: src/views/site/site_list/SiteList.vue:60
 #: src/views/stream/components/RightSettings.vue:41
@@ -1040,23 +1185,99 @@ msgstr ""
 msgid "Failed to enable %{msg}"
 msgstr ""
 
+#: src/constants/errors/backup.ts:23
+msgid "Failed to encrypt data: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:21
+msgid "Failed to encrypt file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:8
+msgid "Failed to encrypt Nginx directory: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:7
+msgid "Failed to encrypt Nginx UI directory: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:33
+msgid "Failed to extract archive: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:10
+msgid "Failed to generate AES key: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:11
+msgid "Failed to generate initialization vector: {0}"
+msgstr ""
+
 #: src/language/constants.ts:5
 msgid "Failed to get certificate information"
 msgstr ""
 
+#: src/constants/errors/backup.ts:28
+msgid "Failed to open source file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:47
+msgid "Failed to open zip entry: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:43
+msgid "Failed to open zip file: {0}"
+msgstr ""
+
 #: src/constants/errors/self_check.ts:4
 msgid "Failed to parse nginx.conf"
 msgstr ""
 
+#: src/constants/errors/backup.ts:48
+msgid "Failed to read encrypted file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:20
+msgid "Failed to read file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:51
+msgid "Failed to read hash info file: {0}"
+msgstr ""
+
 #: src/constants/errors/self_check.ts:3
 msgid "Failed to read nginx.conf"
 msgstr ""
 
+#: src/constants/errors/backup.ts:37
+msgid "Failed to restore Nginx configs: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:38
+msgid "Failed to restore Nginx UI files: {0}"
+msgstr ""
+
 #: src/views/site/site_edit/SiteEdit.vue:135
 #: src/views/stream/StreamEdit.vue:122
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr ""
 
+#: src/constants/errors/backup.ts:36
+msgid "Failed to verify hashes: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:50
+msgid "Failed to write decrypted file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:22
+msgid "Failed to write encrypted file: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:31
+msgid "Failed to write to zip buffer: {0}"
+msgstr ""
+
 #: src/language/constants.ts:32
 msgid "File exists"
 msgstr ""
@@ -1074,7 +1295,7 @@ msgid "Filter"
 msgstr ""
 
 #: src/language/constants.ts:19
-#: src/views/site/SiteAdd.vue:97
+#: src/views/site/site_add/SiteAdd.vue:97
 msgid "Finished"
 msgstr ""
 
@@ -1145,11 +1366,15 @@ msgstr ""
 msgid "Github Proxy"
 msgstr ""
 
+#: src/constants/errors/backup.ts:54
+msgid "Hash verification failed: file integrity compromised"
+msgstr ""
+
 #: src/components/SensitiveString/SensitiveString.vue:40
 msgid "Hide"
 msgstr ""
 
-#: src/routes/index.ts:30
+#: src/routes/index.ts:47
 msgid "Home"
 msgstr ""
 
@@ -1193,7 +1418,7 @@ msgstr ""
 msgid "Import"
 msgstr ""
 
-#: src/routes/index.ts:174
+#: src/routes/modules/certificates.ts:46
 #: src/views/certificate/CertificateEditor.vue:85
 msgid "Import Certificate"
 msgstr ""
@@ -1220,7 +1445,7 @@ msgstr ""
 msgid "Input the recovery code:"
 msgstr ""
 
-#: src/routes/index.ts:312
+#: src/routes/modules/auth.ts:8
 #: src/views/other/Install.vue:134
 msgid "Install"
 msgstr ""
@@ -1237,6 +1462,18 @@ msgstr ""
 msgid "Invalid"
 msgstr ""
 
+#: src/constants/errors/backup.ts:42
+msgid "Invalid AES IV format: {0}"
+msgstr ""
+
+#: src/constants/errors/backup.ts:41
+msgid "Invalid AES key format: {0}"
+msgstr ""
+
+#: src/views/system/Backup/SystemRestore.vue:67
+msgid "Invalid file object"
+msgstr ""
+
 #: src/views/config/components/Rename.vue:64
 #: src/views/config/ConfigEditor.vue:250
 msgid "Invalid filename"
@@ -1250,6 +1487,10 @@ msgstr ""
 msgid "Invalid otp code"
 msgstr ""
 
+#: src/constants/errors/backup.ts:25
+msgid "Invalid padding in decrypted data"
+msgstr ""
+
 #: src/components/TwoFA/use2FAModal.ts:61
 msgid "Invalid passcode or recovery code"
 msgstr ""
@@ -1262,6 +1503,10 @@ msgstr ""
 msgid "Invalid request format"
 msgstr ""
 
+#: src/constants/errors/backup.ts:40
+msgid "Invalid security token format"
+msgstr ""
+
 #: src/views/preference/AuthSettings.vue:18
 msgid "IP"
 msgstr ""
@@ -1374,7 +1619,7 @@ msgstr ""
 msgid "Log"
 msgstr ""
 
-#: src/routes/index.ts:318
+#: src/routes/modules/auth.ts:14
 #: src/views/other/Login.vue:222
 msgid "Login"
 msgstr ""
@@ -1400,24 +1645,24 @@ msgstr ""
 msgid "Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort before obtaining the certificate."
 msgstr ""
 
-#: src/routes/index.ts:108
+#: src/routes/modules/config.ts:10
 #: src/views/config/ConfigEditor.vue:102
 #: src/views/config/ConfigEditor.vue:139
 #: src/views/config/ConfigList.vue:69
 msgid "Manage Configs"
 msgstr ""
 
-#: src/routes/index.ts:47
+#: src/routes/modules/sites.ts:10
 #: src/views/site/site_list/SiteList.vue:94
 msgid "Manage Sites"
 msgstr ""
 
-#: src/routes/index.ts:89
+#: src/routes/modules/streams.ts:10
 #: src/views/stream/StreamList.vue:119
 msgid "Manage Streams"
 msgstr ""
 
-#: src/routes/index.ts:257
+#: src/routes/modules/user.ts:10
 #: src/views/user/User.vue:10
 msgid "Manage Users"
 msgstr ""
@@ -1454,12 +1699,12 @@ msgstr ""
 msgid "Modify"
 msgstr ""
 
-#: src/routes/index.ts:164
+#: src/routes/modules/certificates.ts:36
 #: src/views/certificate/CertificateEditor.vue:85
 msgid "Modify Certificate"
 msgstr ""
 
-#: src/views/site/SiteAdd.vue:154
+#: src/views/site/site_add/SiteAdd.vue:154
 msgid "Modify Config"
 msgstr ""
 
@@ -1522,7 +1767,7 @@ msgstr ""
 
 #: src/views/certificate/WildcardCertificate.vue:91
 #: src/views/site/cert/components/ObtainCert.vue:211
-#: src/views/site/SiteAdd.vue:141
+#: src/views/site/site_add/SiteAdd.vue:141
 msgid "Next"
 msgstr ""
 
@@ -1558,6 +1803,14 @@ msgstr ""
 msgid "Nginx conf not include stream-enabled"
 msgstr ""
 
+#: src/constants/errors/backup.ts:18
+msgid "Nginx config directory is not set"
+msgstr ""
+
+#: src/views/system/Backup/SystemRestore.vue:84
+msgid "Nginx configuration has been restored"
+msgstr ""
+
 #: src/views/site/site_edit/SiteEdit.vue:223
 #: src/views/stream/StreamEdit.vue:208
 msgid "Nginx Configuration Parse Error"
@@ -1579,7 +1832,7 @@ msgstr ""
 msgid "Nginx is not running"
 msgstr ""
 
-#: src/routes/index.ts:202
+#: src/routes/modules/nginx_log.ts:9
 #: src/views/nginx_log/NginxLog.vue:148
 msgid "Nginx Log"
 msgstr ""
@@ -1608,6 +1861,14 @@ msgstr ""
 msgid "Nginx restarted successfully"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:88
+msgid "Nginx UI configuration has been restored"
+msgstr ""
+
+#: src/views/system/Backup/SystemRestore.vue:93
+msgid "Nginx UI configuration has been restored and will restart automatically in a few seconds."
+msgstr ""
+
 #: src/components/ChatGPT/ChatGPT.vue:374
 #: src/components/Notification/Notification.vue:133
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:63
@@ -1637,7 +1898,7 @@ msgstr ""
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:324
+#: src/routes/modules/error.ts:8
 msgid "Not Found"
 msgstr ""
 
@@ -1659,7 +1920,7 @@ msgid "Notification"
 msgstr ""
 
 #: src/components/Notification/Notification.vue:131
-#: src/routes/index.ts:248
+#: src/routes/modules/notifications.ts:10
 msgid "Notifications"
 msgstr ""
 
@@ -1705,6 +1966,8 @@ msgstr ""
 #: src/views/site/site_list/SiteList.vue:144
 #: src/views/stream/components/RightSettings.vue:50
 #: src/views/stream/StreamList.vue:164
+#: src/views/system/Backup/BackupCreator.vue:149
+#: src/views/system/Backup/SystemRestore.vue:94
 msgid "OK"
 msgstr ""
 
@@ -1719,6 +1982,10 @@ msgstr ""
 msgid "Online"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:24
+msgid "Only zip files are allowed"
+msgstr ""
+
 #: src/views/preference/Preference.vue:170
 msgid "OpenAI"
 msgstr ""
@@ -1824,6 +2091,14 @@ msgstr ""
 msgid "Please enter the OTP code:"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:58
+msgid "Please enter the security token"
+msgstr ""
+
+#: src/views/system/Backup/SystemRestore.vue:153
+msgid "Please enter the security token received during backup"
+msgstr ""
+
 #: src/views/certificate/DNSCredential.vue:53
 msgid "Please fill in the API authentication credentials provided by your DNS provider."
 msgstr ""
@@ -1876,6 +2151,14 @@ msgstr ""
 msgid "Please note that the unit of time configurations below are all in seconds."
 msgstr ""
 
+#: src/views/system/Backup/BackupCreator.vue:107
+msgid "Please save this security token, you will need it for restoration:"
+msgstr ""
+
+#: src/views/system/Backup/SystemRestore.vue:53
+msgid "Please select a backup file"
+msgstr ""
+
 #: src/views/environment/Environment.vue:58
 msgid "Please select at least one node to upgrade"
 msgstr ""
@@ -1887,7 +2170,7 @@ msgstr ""
 msgid "Pre-release"
 msgstr ""
 
-#: src/routes/index.ts:266
+#: src/routes/modules/preference.ts:10
 #: src/views/preference/Preference.vue:141
 msgid "Preference"
 msgstr ""
@@ -2142,6 +2425,18 @@ msgstr ""
 msgid "Restarting"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:81
+msgid "Restore completed successfully"
+msgstr ""
+
+#: src/views/system/Backup/SystemRestore.vue:165
+msgid "Restore Nginx Configuration"
+msgstr ""
+
+#: src/views/system/Backup/SystemRestore.vue:176
+msgid "Restore Nginx UI Configuration"
+msgstr ""
+
 #: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr ""
@@ -2184,7 +2479,7 @@ msgstr ""
 
 #: src/views/config/ConfigEditor.vue:171
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:41
-#: src/views/site/SiteAdd.vue:46
+#: src/views/site/site_add/SiteAdd.vue:46
 msgid "Save error %{msg}"
 msgstr ""
 
@@ -2231,8 +2526,8 @@ msgstr ""
 
 #: src/views/config/ConfigEditor.vue:167
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:39
+#: src/views/site/site_add/SiteAdd.vue:37
 #: src/views/site/site_edit/SiteEdit.vue:152
-#: src/views/site/SiteAdd.vue:37
 #: src/views/stream/StreamEdit.vue:139
 msgid "Saved successfully"
 msgstr ""
@@ -2249,11 +2544,19 @@ msgstr ""
 msgid "Secret has been copied"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:150
+msgid "Security Token"
+msgstr ""
+
+#: src/views/system/Backup/BackupCreator.vue:94
+msgid "Security Token Information"
+msgstr ""
+
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:189
 msgid "Selector"
 msgstr ""
 
-#: src/routes/index.ts:283
+#: src/routes/modules/system.ts:19
 #: src/views/system/SelfCheck/SelfCheck.vue:42
 msgid "Self Check"
 msgstr ""
@@ -2315,12 +2618,12 @@ msgstr ""
 msgid "Single Directive"
 msgstr ""
 
-#: src/routes/index.ts:71
+#: src/routes/modules/sites.ts:34
 #: src/views/site/site_category/SiteCategory.vue:10
 msgid "Site Categories"
 msgstr ""
 
-#: src/views/site/SiteAdd.vue:147
+#: src/views/site/site_add/SiteAdd.vue:147
 msgid "Site Config Created Successfully"
 msgstr ""
 
@@ -2328,7 +2631,7 @@ msgstr ""
 msgid "Site is enabled"
 msgstr ""
 
-#: src/routes/index.ts:224
+#: src/routes/modules/nginx_log.ts:31
 msgid "Site Logs"
 msgstr ""
 
@@ -2340,7 +2643,7 @@ msgstr ""
 msgid "Sites Directory"
 msgstr ""
 
-#: src/routes/index.ts:56
+#: src/routes/modules/sites.ts:19
 msgid "Sites List"
 msgstr ""
 
@@ -2379,6 +2682,10 @@ msgstr ""
 msgid "Stable"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:187
+msgid "Start Restore"
+msgstr ""
+
 #: src/views/certificate/ACMEUser.vue:65
 #: src/views/certificate/CertificateList/certColumns.tsx:68
 #: src/views/environment/envColumns.tsx:44
@@ -2425,6 +2732,10 @@ msgstr ""
 msgid "Support communication with the backend through the WebSocket protocol. If your Nginx UI is being used via an Nginx reverse proxy, please refer to this link to write the corresponding configuration file: https://nginxui.com/guide/nginx-proxy-example.html"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:140
+msgid "Supported file type: .zip"
+msgstr ""
+
 #: src/views/dashboard/ServerAnalytic.vue:236
 #: src/views/dashboard/ServerAnalytic.vue:237
 msgid "Swap"
@@ -2483,7 +2794,7 @@ msgstr ""
 msgid "Sync Config Success"
 msgstr ""
 
-#: src/views/site/site_category/SiteCategory.vue:17
+#: src/views/site/site_category/SiteCategory.vue:18
 msgid "Sync Nodes"
 msgstr ""
 
@@ -2500,19 +2811,27 @@ msgstr ""
 msgid "Synchronization"
 msgstr ""
 
-#: src/routes/index.ts:275
+#: src/routes/modules/system.ts:11
 msgid "System"
 msgstr ""
 
+#: src/views/system/Backup/BackupCreator.vue:71
+msgid "System Backup"
+msgstr ""
+
 #: src/views/certificate/ACMEUserSelector.vue:88
 msgid "System Initial User"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:117
+msgid "System Restore"
+msgstr ""
+
 #: src/constants/errors/self_check.ts:2
 msgid "Task not found"
 msgstr ""
 
-#: src/routes/index.ts:194
+#: src/routes/modules/terminal.ts:10
 #: src/views/terminal/Terminal.vue:114
 msgid "Terminal"
 msgstr ""
@@ -2553,7 +2872,7 @@ msgstr ""
 msgid "The node name should only contain letters, unicode, numbers, hyphens, dashes, colons, and dots."
 msgstr ""
 
-#: src/views/site/SiteAdd.vue:120
+#: src/views/site/site_add/SiteAdd.vue:120
 msgid "The parameter of server_name is required"
 msgstr ""
 
@@ -2626,10 +2945,22 @@ msgstr ""
 msgid "This field should only contain letters, unicode characters, numbers, and -_."
 msgstr ""
 
+#: src/views/system/Backup/BackupCreator.vue:141
+msgid "This token will only be shown once and cannot be retrieved later. Please make sure to save it in a secure location."
+msgstr ""
+
 #: src/constants/form_errors.ts:4
 msgid "This value is already taken"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:169
+msgid "This will restore all Nginx configuration files. Nginx will restart after the restoration is complete."
+msgstr ""
+
+#: src/views/system/Backup/SystemRestore.vue:180
+msgid "This will restore configuration files and database. Nginx UI will restart after the restoration is complete."
+msgstr ""
+
 #: src/views/environment/BatchUpgrader.vue:182
 msgid "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
 msgstr ""
@@ -2717,7 +3048,7 @@ msgstr ""
 msgid "Updated successfully"
 msgstr ""
 
-#: src/routes/index.ts:297
+#: src/routes/modules/system.ts:40
 #: src/views/environment/Environment.vue:66
 #: src/views/system/Upgrade.vue:143
 #: src/views/system/Upgrade.vue:226
@@ -2784,6 +3115,10 @@ msgstr ""
 msgid "Valid"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:159
+msgid "Verify Backup File Integrity"
+msgstr ""
+
 #: src/views/environment/envColumns.tsx:31
 msgid "Version"
 msgstr ""
@@ -2817,10 +3152,15 @@ msgstr ""
 #: src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
 #: src/views/preference/components/AddPasskey.vue:82
-#: src/views/site/SiteAdd.vue:115
+#: src/views/site/site_add/SiteAdd.vue:115
+#: src/views/system/Backup/BackupCreator.vue:138
 msgid "Warning"
 msgstr ""
 
+#: src/views/system/Backup/SystemRestore.vue:121
+msgid "Warning: Restore operation will overwrite current configurations. Make sure you have a valid backup file and security token, and carefully select what to restore."
+msgstr ""
+
 #: src/views/certificate/DNSCredential.vue:56
 msgid "We will add one or more TXT records to the DNS records of your domain for ownership verification."
 msgstr ""

Різницю між файлами не показано, бо вона завелика
+ 425 - 100
app/src/language/ru_RU/app.po


Різницю між файлами не показано, бо вона завелика
+ 448 - 50
app/src/language/tr_TR/app.po


Різницю між файлами не показано, бо вона завелика
+ 444 - 50
app/src/language/vi_VN/app.po


+ 419 - 55
app/src/language/zh_CN/app.po

@@ -3,7 +3,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: \n"
 "POT-Creation-Date: \n"
-"PO-Revision-Date: 2025-03-03 17:51+0800\n"
+"PO-Revision-Date: 2025-03-29 18:45+0800\n"
 "Last-Translator: 0xJacky <me@jackyu.cn>\n"
 "Language-Team: Chinese (Simplified Han script) <https://weblate.nginxui.com/"
 "projects/nginx-ui/frontend/zh_Hans/>\n"
@@ -23,15 +23,15 @@ msgstr "2FA"
 msgid "2FA Settings"
 msgstr "2FA 设置"
 
-#: src/routes/index.ts:290
+#: src/routes/modules/system.ts:26
 msgid "About"
 msgstr "关于"
 
-#: src/routes/index.ts:210 src/views/site/ngx_conf/LogEntry.vue:75
+#: src/routes/modules/nginx_log.ts:17 src/views/site/ngx_conf/LogEntry.vue:75
 msgid "Access Logs"
 msgstr "访问日志"
 
-#: src/routes/index.ts:148 src/views/certificate/ACMEUser.vue:113
+#: src/routes/modules/certificates.ts:20 src/views/certificate/ACMEUser.vue:113
 #: src/views/certificate/ACMEUserSelector.vue:85
 msgid "ACME User"
 msgstr "ACME 用户"
@@ -43,7 +43,7 @@ msgstr "ACME 用户"
 #: src/views/environment/envColumns.tsx:97
 #: src/views/notification/notificationColumns.tsx:65
 #: src/views/preference/AuthSettings.vue:30
-#: src/views/site/site_category/columns.ts:29
+#: src/views/site/site_category/columns.ts:28
 #: src/views/site/site_list/columns.tsx:76 src/views/stream/StreamList.vue:49
 #: src/views/user/userColumns.tsx:60
 msgid "Action"
@@ -64,7 +64,7 @@ msgstr "添加"
 msgid "Add a passkey"
 msgstr "添加 Passkey"
 
-#: src/routes/index.ts:118 src/views/config/ConfigEditor.vue:144
+#: src/routes/modules/config.ts:20 src/views/config/ConfigEditor.vue:144
 #: src/views/config/ConfigEditor.vue:204
 msgid "Add Configuration"
 msgstr "添加配置"
@@ -78,7 +78,7 @@ msgstr "在下面添加指令"
 msgid "Add Location"
 msgstr "添加 Location"
 
-#: src/routes/index.ts:63 src/views/site/SiteAdd.vue:89
+#: src/routes/modules/sites.ts:26 src/views/site/site_add/SiteAdd.vue:89
 msgid "Add Site"
 msgstr "添加站点"
 
@@ -248,6 +248,10 @@ msgstr "成功关闭 %{name} 自动续签"
 msgid "Auto-renewal enabled for %{name}"
 msgstr "成功启用 %{name} 自动续签"
 
+#: src/views/system/Backup/SystemRestore.vue:92
+msgid "Automatic Restart"
+msgstr "自动重启"
+
 #: src/views/certificate/CertificateEditor.vue:255
 #: src/views/config/ConfigEditor.vue:213 src/views/config/ConfigList.vue:106
 #: src/views/config/ConfigList.vue:180 src/views/nginx_log/NginxLog.vue:173
@@ -264,6 +268,22 @@ msgstr "返回首页"
 msgid "Back to list"
 msgstr "返回列表"
 
+#: src/routes/modules/system.ts:33
+msgid "Backup"
+msgstr "备份"
+
+#: src/views/system/Backup/SystemRestore.vue:100
+msgid "Backup file integrity check failed, it may have been tampered with"
+msgstr "备份文件完整性检查失败,可能已被篡改"
+
+#: src/constants/errors/backup.ts:39
+msgid "Backup file not found: {0}"
+msgstr "未找到备份文件:{0}"
+
+#: src/views/system/Backup/BackupCreator.vue:42
+msgid "Backup has been downloaded successfully"
+msgstr "已成功下载备份"
+
 #: src/views/preference/AuthSettings.vue:129
 msgid "Ban Threshold Minutes"
 msgstr "禁止阈值(分钟)"
@@ -276,7 +296,7 @@ msgstr "禁止 IP 列表"
 msgid "Banned Until"
 msgstr "禁用至"
 
-#: src/views/site/SiteAdd.vue:95
+#: src/views/site/site_add/SiteAdd.vue:95
 msgid "Base information"
 msgstr "基本信息"
 
@@ -382,12 +402,12 @@ msgid "Certificate Status"
 msgid_plural "Certificates Status"
 msgstr[0] "证书状态"
 
-#: src/routes/index.ts:139
+#: src/routes/modules/certificates.ts:11
 #: src/views/certificate/CertificateList/Certificate.vue:13
 msgid "Certificates"
 msgstr "证书"
 
-#: src/routes/index.ts:156
+#: src/routes/modules/certificates.ts:28
 msgid "Certificates List"
 msgstr "证书列表"
 
@@ -457,6 +477,10 @@ msgstr "清空"
 msgid "Cleared successfully"
 msgstr "清除成功"
 
+#: src/views/system/Backup/SystemRestore.vue:137
+msgid "Click or drag backup file to this area to upload"
+msgstr "单击或拖动备份文件到此区域上传"
+
 #: src/views/preference/components/TOTP.vue:110
 msgid "Click to copy"
 msgstr "点击复制"
@@ -472,6 +496,10 @@ msgstr "命令"
 msgid "Comments"
 msgstr "注释"
 
+#: src/constants/errors/backup.ts:13
+msgid "Config path is empty"
+msgstr "配置路径为空"
+
 #: src/views/site/ngx_conf/config_template/ConfigTemplate.vue:84
 msgid "Config Templates"
 msgstr "配置"
@@ -480,7 +508,7 @@ msgstr "配置"
 msgid "Configuration file is test successful"
 msgstr "配置文件测试成功"
 
-#: src/views/site/SiteAdd.vue:101
+#: src/views/site/site_add/SiteAdd.vue:101
 msgid "Configuration Name"
 msgstr "配置名称"
 
@@ -488,7 +516,7 @@ msgstr "配置名称"
 msgid "Configurations"
 msgstr "配置"
 
-#: src/views/site/SiteAdd.vue:96
+#: src/views/site/site_add/SiteAdd.vue:96
 msgid "Configure SSL"
 msgstr "配置 SSL"
 
@@ -512,7 +540,12 @@ msgstr "内容"
 msgid "Copied"
 msgstr "已拷贝"
 
+#: src/views/system/Backup/BackupCreator.vue:128
+msgid "Copied!"
+msgstr "已拷贝!"
+
 #: src/components/SensitiveString/SensitiveString.vue:37
+#: src/views/system/Backup/BackupCreator.vue:128
 msgid "Copy"
 msgstr "拷贝"
 
@@ -536,10 +569,14 @@ msgstr "CPU:"
 msgid "Create"
 msgstr "创建"
 
-#: src/views/site/SiteAdd.vue:157
+#: src/views/site/site_add/SiteAdd.vue:157
 msgid "Create Another"
 msgstr "再创建一个"
 
+#: src/views/system/Backup/BackupCreator.vue:86
+msgid "Create Backup"
+msgstr "创建备份"
+
 #: src/views/config/ConfigList.vue:116
 msgid "Create File"
 msgstr "创建文件"
@@ -548,9 +585,16 @@ msgstr "创建文件"
 msgid "Create Folder"
 msgstr "创建文件夹"
 
+#: src/views/system/Backup/BackupCreator.vue:75
+msgid ""
+"Create system backups including Nginx configuration and Nginx UI settings. "
+"Backup files will be automatically downloaded to your computer."
+msgstr ""
+"创建系统备份,包括 Nginx 配置和 Nginx UI 设置。备份文件将自动下载到你的电脑。"
+
 #: src/views/notification/notificationColumns.tsx:58
 #: src/views/preference/components/Passkey.vue:95
-#: src/views/site/site_category/columns.ts:15 src/views/user/userColumns.tsx:48
+#: src/views/site/site_category/columns.ts:16 src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgstr "创建时间"
 
@@ -593,7 +637,7 @@ msgid ""
 "indicator."
 msgstr "自定义显示在环境指示器中的本地服务器名称。"
 
-#: src/routes/index.ts:38 src/views/config/ConfigEditor.vue:134
+#: src/routes/modules/dashboard.ts:10 src/views/config/ConfigEditor.vue:134
 #: src/views/config/ConfigEditor.vue:97 src/views/config/ConfigList.vue:64
 msgid "Dashboard"
 msgstr "仪表盘"
@@ -770,7 +814,8 @@ msgstr "禁用成功"
 msgid "Disk IO"
 msgstr "磁盘 IO"
 
-#: src/routes/index.ts:184 src/views/certificate/DNSCredential.vue:40
+#: src/routes/modules/certificates.ts:56
+#: src/views/certificate/DNSCredential.vue:40
 msgid "DNS Credentials"
 msgstr "DNS 凭证"
 
@@ -874,15 +919,15 @@ msgstr "编辑"
 msgid "Edit %{n}"
 msgstr "编辑 %{n}"
 
-#: src/routes/index.ts:128 src/views/config/ConfigEditor.vue:204
+#: src/routes/modules/config.ts:30 src/views/config/ConfigEditor.vue:204
 msgid "Edit Configuration"
 msgstr "编辑配置"
 
-#: src/routes/index.ts:78
+#: src/routes/modules/sites.ts:41
 msgid "Edit Site"
 msgstr "编辑站点"
 
-#: src/routes/index.ts:98
+#: src/routes/modules/streams.ts:19
 msgid "Edit Stream"
 msgstr "编辑 Stream"
 
@@ -907,7 +952,7 @@ msgstr "二步验证启用成功"
 msgid "Enable auto-renewal failed for %{name}"
 msgstr "启用 %{name} 自动续签失败"
 
-#: src/views/site/SiteAdd.vue:43
+#: src/views/site/site_add/SiteAdd.vue:43
 msgid "Enable failed"
 msgstr "启用失败"
 
@@ -964,8 +1009,9 @@ msgstr "启用 TOTP"
 msgid "Enabled"
 msgstr "启用"
 
+#: src/views/site/site_add/SiteAdd.vue:40
 #: src/views/site/site_edit/RightSettings.vue:33
-#: src/views/site/site_list/SiteList.vue:46 src/views/site/SiteAdd.vue:40
+#: src/views/site/site_list/SiteList.vue:46
 #: src/views/stream/components/RightSettings.vue:29
 #: src/views/stream/StreamList.vue:61
 msgid "Enabled successfully"
@@ -979,7 +1025,8 @@ msgstr "用 Let's Encrypt 对网站进行加密"
 msgid "Environment variables cleaned"
 msgstr "环境变量已清理"
 
-#: src/routes/index.ts:234 src/views/dashboard/Environments.vue:83
+#: src/routes/modules/environments.ts:11
+#: src/views/dashboard/Environments.vue:83
 #: src/views/environment/Environment.vue:43
 msgid "Environments"
 msgstr "环境"
@@ -989,7 +1036,7 @@ msgstr "环境"
 msgid "Error"
 msgstr "错误"
 
-#: src/routes/index.ts:217 src/views/site/ngx_conf/LogEntry.vue:83
+#: src/routes/modules/nginx_log.ts:24 src/views/site/ngx_conf/LogEntry.vue:83
 msgid "Error Logs"
 msgstr "错误日志"
 
@@ -1014,10 +1061,114 @@ msgstr "导出"
 msgid "Fail to obtain certificate"
 msgstr "获取证书失败"
 
+#: src/constants/errors/backup.ts:5
+msgid "Failed to backup Nginx config files: {0}"
+msgstr "备份 Nginx 配置文件失败:{0}"
+
+#: src/constants/errors/backup.ts:4
+msgid "Failed to backup Nginx UI files: {0}"
+msgstr "备份 Nginx UI 文件失败:{0}"
+
+#: src/constants/errors/backup.ts:17
+msgid "Failed to calculate hash: {0}"
+msgstr "计算哈希值失败:{0}"
+
+#: src/constants/errors/backup.ts:53
+msgid "Failed to calculate Nginx hash: {0}"
+msgstr "计算 Nginx 哈希值失败:{0}"
+
+#: src/constants/errors/backup.ts:52
+msgid "Failed to calculate Nginx UI hash: {0}"
+msgstr "计算 Nginx UI 哈希值失败:{0}"
+
+#: src/constants/errors/backup.ts:14
+msgid "Failed to copy config file: {0}"
+msgstr "复制配置文件失败:{0}"
+
+#: src/constants/errors/backup.ts:15
+msgid "Failed to copy database directory: {0}"
+msgstr "复制数据库目录失败:{0}"
+
+#: src/constants/errors/backup.ts:16
+msgid "Failed to copy database file: {0}"
+msgstr "复制数据库文件失败:{0}"
+
+#: src/constants/errors/backup.ts:30
+msgid "Failed to copy file content: {0}"
+msgstr "复制文件内容失败:{0}"
+
+#: src/constants/errors/backup.ts:19
+msgid "Failed to copy Nginx config directory: {0}"
+msgstr "复制 Nginx 配置目录失败:{0}"
+
 #: src/constants/errors/self_check.ts:9
 msgid "Failed to create backup"
 msgstr "创建备份失败"
 
+#: src/constants/errors/backup.ts:12
+msgid "Failed to create backup file: {0}"
+msgstr "创建备份文件失败:{0}"
+
+#: src/constants/errors/backup.ts:44
+msgid "Failed to create directory: {0}"
+msgstr "创建目录失败:{0}"
+
+#: src/constants/errors/backup.ts:46
+msgid "Failed to create file: {0}"
+msgstr "创建文件失败:{0}"
+
+#: src/constants/errors/backup.ts:6
+msgid "Failed to create hash info file: {0}"
+msgstr "创建哈希信息文件失败:{0}"
+
+#: src/constants/errors/backup.ts:45
+msgid "Failed to create parent directory: {0}"
+msgstr "创建父目录失败:{0}"
+
+#: src/constants/errors/backup.ts:32
+msgid "Failed to create restore directory: {0}"
+msgstr "创建还原目录失败:{0}"
+
+#: src/constants/errors/backup.ts:2
+msgid "Failed to create temporary directory"
+msgstr "创建临时目录失败"
+
+#: src/constants/errors/backup.ts:3
+msgid "Failed to create temporary subdirectory"
+msgstr "创建临时子目录失败"
+
+#: src/constants/errors/backup.ts:9
+msgid "Failed to create zip archive: {0}"
+msgstr "创建 zip 压缩包失败:{0}"
+
+#: src/constants/errors/backup.ts:27
+msgid "Failed to create zip entry: {0}"
+msgstr "创建 zip 条目失败:{0}"
+
+#: src/constants/errors/backup.ts:26
+msgid "Failed to create zip file: {0}"
+msgstr "创建压缩文件失败:{0}"
+
+#: src/constants/errors/backup.ts:29
+msgid "Failed to create zip header: {0}"
+msgstr "创建 zip 头失败:{0}"
+
+#: src/constants/errors/backup.ts:24
+msgid "Failed to decrypt data: {0}"
+msgstr "解密数据失败:{0}"
+
+#: src/constants/errors/backup.ts:49
+msgid "Failed to decrypt file: {0}"
+msgstr "解密文件失败:{0}"
+
+#: src/constants/errors/backup.ts:35
+msgid "Failed to decrypt Nginx directory: {0}"
+msgstr "解密 Nginx 目录失败:{0}"
+
+#: src/constants/errors/backup.ts:34
+msgid "Failed to decrypt Nginx UI directory: {0}"
+msgstr "解密 Nginx UI 目录失败:{0}"
+
 #: src/views/site/site_edit/RightSettings.vue:45
 #: src/views/site/site_list/SiteList.vue:60
 #: src/views/stream/components/RightSettings.vue:41
@@ -1032,23 +1183,99 @@ msgstr "禁用失败 %{msg}"
 msgid "Failed to enable %{msg}"
 msgstr "启用失败 %{msg}"
 
+#: src/constants/errors/backup.ts:23
+msgid "Failed to encrypt data: {0}"
+msgstr "加密数据失败:{0}"
+
+#: src/constants/errors/backup.ts:21
+msgid "Failed to encrypt file: {0}"
+msgstr "加密文件失败:{0}"
+
+#: src/constants/errors/backup.ts:8
+msgid "Failed to encrypt Nginx directory: {0}"
+msgstr "加密 Nginx 目录失败:{0}"
+
+#: src/constants/errors/backup.ts:7
+msgid "Failed to encrypt Nginx UI directory: {0}"
+msgstr "加密 Nginx UI 目录失败:{0}"
+
+#: src/constants/errors/backup.ts:33
+msgid "Failed to extract archive: {0}"
+msgstr "解压缩失败:{0}"
+
+#: src/constants/errors/backup.ts:10
+msgid "Failed to generate AES key: {0}"
+msgstr "生成 AES 密钥失败: {0}"
+
+#: src/constants/errors/backup.ts:11
+msgid "Failed to generate initialization vector: {0}"
+msgstr "生成初始化向量失败:{0}"
+
 #: src/language/constants.ts:5
 msgid "Failed to get certificate information"
 msgstr "获取证书信息失败"
 
+#: src/constants/errors/backup.ts:28
+msgid "Failed to open source file: {0}"
+msgstr "打开源文件失败:{0}"
+
+#: src/constants/errors/backup.ts:47
+msgid "Failed to open zip entry: {0}"
+msgstr "打开 zip 条目失败:{0}"
+
+#: src/constants/errors/backup.ts:43
+msgid "Failed to open zip file: {0}"
+msgstr "打开压缩文件失败:{0}"
+
 #: src/constants/errors/self_check.ts:4
 msgid "Failed to parse nginx.conf"
 msgstr "解析 nginx.conf 失败"
 
+#: src/constants/errors/backup.ts:48
+msgid "Failed to read encrypted file: {0}"
+msgstr "读取加密文件失败:{0}"
+
+#: src/constants/errors/backup.ts:20
+msgid "Failed to read file: {0}"
+msgstr "读取文件失败:{0}"
+
+#: src/constants/errors/backup.ts:51
+msgid "Failed to read hash info file: {0}"
+msgstr "读取哈希信息文件失败:{0}"
+
 #: src/constants/errors/self_check.ts:3
 msgid "Failed to read nginx.conf"
 msgstr "读取 nginx.conf 失败"
 
+#: src/constants/errors/backup.ts:37
+msgid "Failed to restore Nginx configs: {0}"
+msgstr "恢复 Nginx 配置失败:{0}"
+
+#: src/constants/errors/backup.ts:38
+msgid "Failed to restore Nginx UI files: {0}"
+msgstr "恢复 Nginx UI 文件失败:{0}"
+
 #: src/views/site/site_edit/SiteEdit.vue:135
 #: src/views/stream/StreamEdit.vue:122
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr "保存失败,在配置中检测到语法错误。"
 
+#: src/constants/errors/backup.ts:36
+msgid "Failed to verify hashes: {0}"
+msgstr "验证哈希值失败:{0}"
+
+#: src/constants/errors/backup.ts:50
+msgid "Failed to write decrypted file: {0}"
+msgstr "写入解密文件失败:{0}"
+
+#: src/constants/errors/backup.ts:22
+msgid "Failed to write encrypted file: {0}"
+msgstr "写入加密文件失败:{0}"
+
+#: src/constants/errors/backup.ts:31
+msgid "Failed to write to zip buffer: {0}"
+msgstr "写入 zip 缓冲区失败:{0}"
+
 #: src/language/constants.ts:32
 msgid "File exists"
 msgstr "文件已存在"
@@ -1065,7 +1292,7 @@ msgstr "文件名为空"
 msgid "Filter"
 msgstr "过滤"
 
-#: src/language/constants.ts:19 src/views/site/SiteAdd.vue:97
+#: src/language/constants.ts:19 src/views/site/site_add/SiteAdd.vue:97
 msgid "Finished"
 msgstr "完成"
 
@@ -1137,11 +1364,15 @@ msgstr "正在获取证书,请稍等..."
 msgid "Github Proxy"
 msgstr "Github代理"
 
+#: src/constants/errors/backup.ts:54
+msgid "Hash verification failed: file integrity compromised"
+msgstr "哈希验证失败:文件完整性受损"
+
 #: src/components/SensitiveString/SensitiveString.vue:40
 msgid "Hide"
 msgstr "隐藏"
 
-#: src/routes/index.ts:30
+#: src/routes/index.ts:47
 msgid "Home"
 msgstr "首页"
 
@@ -1191,7 +1422,8 @@ msgstr "如果您的域名有 CNAME 记录且无法获取证书,则需要启
 msgid "Import"
 msgstr "导入"
 
-#: src/routes/index.ts:174 src/views/certificate/CertificateEditor.vue:85
+#: src/routes/modules/certificates.ts:46
+#: src/views/certificate/CertificateEditor.vue:85
 msgid "Import Certificate"
 msgstr "导入证书"
 
@@ -1216,7 +1448,7 @@ msgstr "输入应用程序中的代码:"
 msgid "Input the recovery code:"
 msgstr "输入恢复代码:"
 
-#: src/routes/index.ts:312 src/views/other/Install.vue:134
+#: src/routes/modules/auth.ts:8 src/views/other/Install.vue:134
 msgid "Install"
 msgstr "安装"
 
@@ -1232,6 +1464,18 @@ msgstr "间隔"
 msgid "Invalid"
 msgstr "无效的"
 
+#: src/constants/errors/backup.ts:42
+msgid "Invalid AES IV format: {0}"
+msgstr "AES IV 格式无效:{0}"
+
+#: src/constants/errors/backup.ts:41
+msgid "Invalid AES key format: {0}"
+msgstr "AES 密钥格式无效:{0}"
+
+#: src/views/system/Backup/SystemRestore.vue:67
+msgid "Invalid file object"
+msgstr "无效文件对象"
+
 #: src/views/config/components/Rename.vue:64
 #: src/views/config/ConfigEditor.vue:250
 msgid "Invalid filename"
@@ -1245,6 +1489,10 @@ msgstr "无效文件夹名"
 msgid "Invalid otp code"
 msgstr "无效的 OTP 代码"
 
+#: src/constants/errors/backup.ts:25
+msgid "Invalid padding in decrypted data"
+msgstr "解密数据中的无效填充"
+
 #: src/components/TwoFA/use2FAModal.ts:61
 msgid "Invalid passcode or recovery code"
 msgstr "二次验证码或恢复代码无效"
@@ -1257,6 +1505,10 @@ msgstr "无效的恢复代码"
 msgid "Invalid request format"
 msgstr "无效的请求格式"
 
+#: src/constants/errors/backup.ts:40
+msgid "Invalid security token format"
+msgstr "安全令牌格式无效"
+
 #: src/views/preference/AuthSettings.vue:18
 msgid "IP"
 msgstr "IP"
@@ -1372,7 +1624,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "日志"
 
-#: src/routes/index.ts:318 src/views/other/Login.vue:222
+#: src/routes/modules/auth.ts:14 src/views/other/Login.vue:222
 msgid "Login"
 msgstr "登录"
 
@@ -1410,20 +1662,20 @@ msgstr ""
 "在获取签发证书前,请确保配置文件中已将 .well-known 目录反向代理到 "
 "HTTPChallengePort。"
 
-#: src/routes/index.ts:108 src/views/config/ConfigEditor.vue:102
+#: src/routes/modules/config.ts:10 src/views/config/ConfigEditor.vue:102
 #: src/views/config/ConfigEditor.vue:139 src/views/config/ConfigList.vue:69
 msgid "Manage Configs"
 msgstr "配置管理"
 
-#: src/routes/index.ts:47 src/views/site/site_list/SiteList.vue:94
+#: src/routes/modules/sites.ts:10 src/views/site/site_list/SiteList.vue:94
 msgid "Manage Sites"
 msgstr "网站管理"
 
-#: src/routes/index.ts:89 src/views/stream/StreamList.vue:119
+#: src/routes/modules/streams.ts:10 src/views/stream/StreamList.vue:119
 msgid "Manage Streams"
 msgstr "管理 Stream"
 
-#: src/routes/index.ts:257 src/views/user/User.vue:10
+#: src/routes/modules/user.ts:10 src/views/user/User.vue:10
 msgid "Manage Users"
 msgstr "用户管理"
 
@@ -1459,11 +1711,12 @@ msgstr "模型"
 msgid "Modify"
 msgstr "修改"
 
-#: src/routes/index.ts:164 src/views/certificate/CertificateEditor.vue:85
+#: src/routes/modules/certificates.ts:36
+#: src/views/certificate/CertificateEditor.vue:85
 msgid "Modify Certificate"
 msgstr "修改证书"
 
-#: src/views/site/SiteAdd.vue:154
+#: src/views/site/site_add/SiteAdd.vue:154
 msgid "Modify Config"
 msgstr "修改配置文件"
 
@@ -1524,7 +1777,7 @@ msgstr "新版本发布"
 
 #: src/views/certificate/WildcardCertificate.vue:91
 #: src/views/site/cert/components/ObtainCert.vue:211
-#: src/views/site/SiteAdd.vue:141
+#: src/views/site/site_add/SiteAdd.vue:141
 msgid "Next"
 msgstr "下一步"
 
@@ -1560,6 +1813,14 @@ msgstr "Nginx Conf 中未引用 sites-enabled"
 msgid "Nginx conf not include stream-enabled"
 msgstr "Nginx Conf 中未引用 stream-enabled"
 
+#: src/constants/errors/backup.ts:18
+msgid "Nginx config directory is not set"
+msgstr "未设置 Nginx 配置目录"
+
+#: src/views/system/Backup/SystemRestore.vue:84
+msgid "Nginx configuration has been restored"
+msgstr "Nginx 配置已恢复"
+
 #: src/views/site/site_edit/SiteEdit.vue:223
 #: src/views/stream/StreamEdit.vue:208
 msgid "Nginx Configuration Parse Error"
@@ -1581,7 +1842,7 @@ msgstr "Nginx 错误日志路径"
 msgid "Nginx is not running"
 msgstr "Nginx 未启动"
 
-#: src/routes/index.ts:202 src/views/nginx_log/NginxLog.vue:148
+#: src/routes/modules/nginx_log.ts:9 src/views/nginx_log/NginxLog.vue:148
 msgid "Nginx Log"
 msgstr "Nginx 日志"
 
@@ -1609,6 +1870,16 @@ msgstr "Nginx 重启命令"
 msgid "Nginx restarted successfully"
 msgstr "Nginx 重启成功"
 
+#: src/views/system/Backup/SystemRestore.vue:88
+msgid "Nginx UI configuration has been restored"
+msgstr "Nginx 用户界面配置已恢复"
+
+#: src/views/system/Backup/SystemRestore.vue:93
+msgid ""
+"Nginx UI configuration has been restored and will restart automatically in a "
+"few seconds."
+msgstr "Nginx UI 配置已恢复,几秒钟后将自动重启。"
+
 #: src/components/ChatGPT/ChatGPT.vue:374
 #: src/components/Notification/Notification.vue:133
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:63
@@ -1638,7 +1909,7 @@ msgstr "节点密钥"
 msgid "Not After"
 msgstr "有效期"
 
-#: src/routes/index.ts:324
+#: src/routes/modules/error.ts:8
 msgid "Not Found"
 msgstr "找不到页面"
 
@@ -1661,7 +1932,8 @@ msgstr "注意,如果配置文件中包含其他配置或证书,请提前将
 msgid "Notification"
 msgstr "通知"
 
-#: src/components/Notification/Notification.vue:131 src/routes/index.ts:248
+#: src/components/Notification/Notification.vue:131
+#: src/routes/modules/notifications.ts:10
 msgid "Notifications"
 msgstr "通知"
 
@@ -1709,6 +1981,8 @@ msgstr "确定"
 #: src/views/site/site_list/SiteList.vue:144
 #: src/views/stream/components/RightSettings.vue:50
 #: src/views/stream/StreamList.vue:164
+#: src/views/system/Backup/BackupCreator.vue:149
+#: src/views/system/Backup/SystemRestore.vue:94
 msgid "OK"
 msgstr "确定"
 
@@ -1723,6 +1997,10 @@ msgstr "一旦验证完成,这些记录将被删除。"
 msgid "Online"
 msgstr "在线"
 
+#: src/views/system/Backup/SystemRestore.vue:24
+msgid "Only zip files are allowed"
+msgstr "只允许使用zip文件"
+
 #: src/views/preference/Preference.vue:170
 msgid "OpenAI"
 msgstr "OpenAI"
@@ -1832,6 +2110,14 @@ msgstr "请为您要创建的 Passkey 输入一个名称,然后单击下面的
 msgid "Please enter the OTP code:"
 msgstr "请输入 OTP:"
 
+#: src/views/system/Backup/SystemRestore.vue:58
+msgid "Please enter the security token"
+msgstr "请输入安全令牌"
+
+#: src/views/system/Backup/SystemRestore.vue:153
+msgid "Please enter the security token received during backup"
+msgstr "请输入备份时收到的安全令牌"
+
 #: src/views/certificate/DNSCredential.vue:53
 msgid ""
 "Please fill in the API authentication credentials provided by your DNS "
@@ -1895,6 +2181,14 @@ msgid ""
 "Please note that the unit of time configurations below are all in seconds."
 msgstr "请注意,下面的时间单位配置均以秒为单位。"
 
+#: src/views/system/Backup/BackupCreator.vue:107
+msgid "Please save this security token, you will need it for restoration:"
+msgstr "请保存此安全令牌,恢复时会用到它:"
+
+#: src/views/system/Backup/SystemRestore.vue:53
+msgid "Please select a backup file"
+msgstr "请选择备份文件"
+
 #: src/views/environment/Environment.vue:58
 msgid "Please select at least one node to upgrade"
 msgstr "请至少选择一个节点进行升级"
@@ -1905,7 +2199,7 @@ msgstr "请至少选择一个节点进行升级"
 msgid "Pre-release"
 msgstr "预发布"
 
-#: src/routes/index.ts:266 src/views/preference/Preference.vue:141
+#: src/routes/modules/preference.ts:10 src/views/preference/Preference.vue:141
 msgid "Preference"
 msgstr "偏好设置"
 
@@ -2161,6 +2455,18 @@ msgstr "重启"
 msgid "Restarting"
 msgstr "重启中"
 
+#: src/views/system/Backup/SystemRestore.vue:81
+msgid "Restore completed successfully"
+msgstr "恢复成功"
+
+#: src/views/system/Backup/SystemRestore.vue:165
+msgid "Restore Nginx Configuration"
+msgstr "恢复 Nginx 配置"
+
+#: src/views/system/Backup/SystemRestore.vue:176
+msgid "Restore Nginx UI Configuration"
+msgstr "恢复 Nginx UI 配置"
+
 #: src/views/preference/AuthSettings.vue:107
 msgid "RP Display Name"
 msgstr "依赖方显示名称"
@@ -2203,7 +2509,7 @@ msgstr "保存指令"
 
 #: src/views/config/ConfigEditor.vue:171
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:41
-#: src/views/site/SiteAdd.vue:46
+#: src/views/site/site_add/SiteAdd.vue:46
 msgid "Save error %{msg}"
 msgstr "保存错误 %{msg}"
 
@@ -2248,7 +2554,8 @@ msgstr "保存成功"
 
 #: src/views/config/ConfigEditor.vue:167
 #: src/views/site/ngx_conf/directive/DirectiveEditorItem.vue:39
-#: src/views/site/site_edit/SiteEdit.vue:152 src/views/site/SiteAdd.vue:37
+#: src/views/site/site_add/SiteAdd.vue:37
+#: src/views/site/site_edit/SiteEdit.vue:152
 #: src/views/stream/StreamEdit.vue:139
 msgid "Saved successfully"
 msgstr "保存成功"
@@ -2265,11 +2572,19 @@ msgstr "SDK"
 msgid "Secret has been copied"
 msgstr "密钥已复制"
 
+#: src/views/system/Backup/SystemRestore.vue:150
+msgid "Security Token"
+msgstr "安全 Token"
+
+#: src/views/system/Backup/BackupCreator.vue:94
+msgid "Security Token Information"
+msgstr "安全令牌信息"
+
 #: src/components/StdDesign/StdDataEntry/components/StdSelector.vue:189
 msgid "Selector"
 msgstr "选择器"
 
-#: src/routes/index.ts:283 src/views/system/SelfCheck/SelfCheck.vue:42
+#: src/routes/modules/system.ts:19 src/views/system/SelfCheck/SelfCheck.vue:42
 msgid "Self Check"
 msgstr "自我检查"
 
@@ -2340,11 +2655,12 @@ msgstr "使用 Passkey 登录"
 msgid "Single Directive"
 msgstr "单行指令"
 
-#: src/routes/index.ts:71 src/views/site/site_category/SiteCategory.vue:10
+#: src/routes/modules/sites.ts:34
+#: src/views/site/site_category/SiteCategory.vue:10
 msgid "Site Categories"
 msgstr "网站分类"
 
-#: src/views/site/SiteAdd.vue:147
+#: src/views/site/site_add/SiteAdd.vue:147
 msgid "Site Config Created Successfully"
 msgstr "网站配置创建成功"
 
@@ -2352,7 +2668,7 @@ msgstr "网站配置创建成功"
 msgid "Site is enabled"
 msgstr "网站已启用"
 
-#: src/routes/index.ts:224
+#: src/routes/modules/nginx_log.ts:31
 msgid "Site Logs"
 msgstr "站点列表"
 
@@ -2364,7 +2680,7 @@ msgstr "网站未找到"
 msgid "Sites Directory"
 msgstr "网站目录"
 
-#: src/routes/index.ts:56
+#: src/routes/modules/sites.ts:19
 msgid "Sites List"
 msgstr "站点列表"
 
@@ -2402,6 +2718,10 @@ msgstr "SSO 登录"
 msgid "Stable"
 msgstr "稳定"
 
+#: src/views/system/Backup/SystemRestore.vue:187
+msgid "Start Restore"
+msgstr "开始还原"
+
 #: src/views/certificate/ACMEUser.vue:65
 #: src/views/certificate/CertificateList/certColumns.tsx:68
 #: src/views/environment/envColumns.tsx:44
@@ -2452,6 +2772,10 @@ msgstr ""
 "支持通过 WebSocket 协议与后端通信,如果您正在使用 Nginx 反向代理了 Nginx UI "
 "请参考:https://nginxui.com/guide/nginx-proxy-example.html 编写配置文件"
 
+#: src/views/system/Backup/SystemRestore.vue:140
+msgid "Supported file type: .zip"
+msgstr "支持的文件类型:.zip"
+
 #: src/views/dashboard/ServerAnalytic.vue:236
 #: src/views/dashboard/ServerAnalytic.vue:237
 msgid "Swap"
@@ -2506,7 +2830,7 @@ msgstr "同步配置错误"
 msgid "Sync Config Success"
 msgstr "同步配置成功"
 
-#: src/views/site/site_category/SiteCategory.vue:17
+#: src/views/site/site_category/SiteCategory.vue:18
 msgid "Sync Nodes"
 msgstr "同步节点"
 
@@ -2523,19 +2847,27 @@ msgstr "同步到"
 msgid "Synchronization"
 msgstr "同步"
 
-#: src/routes/index.ts:275
+#: src/routes/modules/system.ts:11
 msgid "System"
 msgstr "系统"
 
+#: src/views/system/Backup/BackupCreator.vue:71
+msgid "System Backup"
+msgstr "系统备份"
+
 #: src/views/certificate/ACMEUserSelector.vue:88
 msgid "System Initial User"
 msgstr "系统初始用户"
 
+#: src/views/system/Backup/SystemRestore.vue:117
+msgid "System Restore"
+msgstr "系统还原"
+
 #: src/constants/errors/self_check.ts:2
 msgid "Task not found"
 msgstr "未找到任务"
 
-#: src/routes/index.ts:194 src/views/terminal/Terminal.vue:114
+#: src/routes/modules/terminal.ts:10 src/views/terminal/Terminal.vue:114
 msgid "Terminal"
 msgstr "终端"
 
@@ -2587,7 +2919,7 @@ msgid ""
 "dashes, colons, and dots."
 msgstr "节点名称只能包含字母、统一码、数字、连字符、破折号、冒号和点。"
 
-#: src/views/site/SiteAdd.vue:120
+#: src/views/site/site_add/SiteAdd.vue:120
 msgid "The parameter of server_name is required"
 msgstr "必须为 server_name 指令指明参数"
 
@@ -2675,10 +3007,28 @@ msgid ""
 "This field should only contain letters, unicode characters, numbers, and -_."
 msgstr "该字段只能包含字母、unicode 字符、数字和 -_。"
 
+#: src/views/system/Backup/BackupCreator.vue:141
+msgid ""
+"This token will only be shown once and cannot be retrieved later. Please "
+"make sure to save it in a secure location."
+msgstr "此 Token 只显示一次,以后无法找回。请确保将其保存在安全位置。"
+
 #: src/constants/form_errors.ts:4
 msgid "This value is already taken"
 msgstr "该字段的值已经存在"
 
+#: src/views/system/Backup/SystemRestore.vue:169
+msgid ""
+"This will restore all Nginx configuration files. Nginx will restart after "
+"the restoration is complete."
+msgstr "这将还原所有 Nginx 配置文件。恢复完成后,Nginx 将重新启动。"
+
+#: src/views/system/Backup/SystemRestore.vue:180
+msgid ""
+"This will restore configuration files and database. Nginx UI will restart "
+"after the restoration is complete."
+msgstr "这将恢复配置文件和数据库。恢复完成后,Nginx UI 将重新启动。"
+
 #: src/views/environment/BatchUpgrader.vue:182
 msgid ""
 "This will upgrade or reinstall the Nginx UI on %{nodeNames} to %{version}."
@@ -2785,7 +3135,7 @@ msgstr "修改时间"
 msgid "Updated successfully"
 msgstr "更新成功"
 
-#: src/routes/index.ts:297 src/views/environment/Environment.vue:66
+#: src/routes/modules/system.ts:40 src/views/environment/Environment.vue:66
 #: src/views/system/Upgrade.vue:143 src/views/system/Upgrade.vue:226
 msgid "Upgrade"
 msgstr "升级"
@@ -2848,6 +3198,10 @@ msgstr "用户名 (*)"
 msgid "Valid"
 msgstr "有效的"
 
+#: src/views/system/Backup/SystemRestore.vue:159
+msgid "Verify Backup File Integrity"
+msgstr "验证备份文件的完整性"
+
 #: src/views/environment/envColumns.tsx:31
 msgid "Version"
 msgstr "版本"
@@ -2880,10 +3234,20 @@ msgstr "已查看"
 #: src/constants/index.ts:17 src/views/config/InspectConfig.vue:33
 #: src/views/notification/notificationColumns.tsx:22
 #: src/views/preference/components/AddPasskey.vue:82
-#: src/views/site/SiteAdd.vue:115
+#: src/views/site/site_add/SiteAdd.vue:115
+#: src/views/system/Backup/BackupCreator.vue:138
 msgid "Warning"
 msgstr "警告"
 
+#: src/views/system/Backup/SystemRestore.vue:121
+msgid ""
+"Warning: Restore operation will overwrite current configurations. Make sure "
+"you have a valid backup file and security token, and carefully select what "
+"to restore."
+msgstr ""
+"警告:还原操作将覆盖当前配置。请确保您有有效的备份文件和安全令牌,并仔细选择"
+"要还原的内容。"
+
 #: src/views/certificate/DNSCredential.vue:56
 msgid ""
 "We will add one or more TXT records to the DNS records of your domain for "
@@ -2986,6 +3350,9 @@ msgstr "您的旧代码将不再有效。"
 msgid "Your passkeys"
 msgstr "你的 Passkeys"
 
+#~ msgid "Restart Required"
+#~ msgstr "必须重新启动"
+
 #~ msgid "Deploy %{conf_name} to %{node_name} successfully"
 #~ msgstr "部署 %{conf_name} 到 %{node_name} 成功"
 
@@ -3017,9 +3384,6 @@ msgstr "你的 Passkeys"
 #~ msgid "Enable successfully"
 #~ msgstr "启用成功"
 
-#~ msgid "Please select at least one node!"
-#~ msgstr "请至少选择一个节点!"
-
 #~ msgid "Please upgrade the remote Nginx UI to the latest version"
 #~ msgstr "请将远程 Nginx UI 升级到最新版本"
 

Різницю між файлами не показано, бо вона завелика
+ 455 - 50
app/src/language/zh_TW/app.po


+ 34 - 5
app/src/lib/http/index.ts

@@ -30,6 +30,19 @@ function registerError(scope: string, record: CosyErrorRecord) {
   errors[scope] = record
 }
 
+export interface HttpConfig extends AxiosRequestConfig {
+  returnFullResponse?: boolean
+  crypto?: boolean
+}
+
+// Extend InternalAxiosRequestConfig type
+declare module 'axios' {
+  interface InternalAxiosRequestConfig {
+    returnFullResponse?: boolean
+    crypto?: boolean
+  }
+}
+
 const instance = axios.create({
   baseURL: import.meta.env.VITE_API_ROOT,
   timeout: 50000,
@@ -37,25 +50,25 @@ const instance = axios.create({
 })
 
 const http = {
-  get(url: string, config: AxiosRequestConfig = {}) {
+  get(url: string, config: HttpConfig = {}) {
     // 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 = {}) {
+  post(url: string, data: any = undefined, config: HttpConfig = {}) {
     // 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 = {}) {
+  put(url: string, data: any = undefined, config: HttpConfig = {}) {
     // eslint-disable-next-line ts/no-explicit-any
     return instance.put<any, any>(url, data, config)
   },
-  delete(url: string, config: AxiosRequestConfig = {}) {
+  delete(url: string, config: HttpConfig = {}) {
     // eslint-disable-next-line ts/no-explicit-any
     return instance.delete<any, any>(url, config)
   },
-  patch(url: string, config: AxiosRequestConfig = {}) {
+  patch(url: string, config: HttpConfig = {}) {
     // eslint-disable-next-line ts/no-explicit-any
     return instance.patch<any, any>(url, config)
   },
@@ -127,6 +140,10 @@ const dedupe = useMessageDedupe()
 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)
   },
   // eslint-disable-next-line sonarjs/cognitive-complexity
@@ -144,6 +161,18 @@ instance.interceptors.response.use(
         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
 
     if (err?.scope) {

+ 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":2,"total_build":387}

+ 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:visible="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;
+}
+
+/* 暗夜模式优化 */
+:deep(.backup-token-modal) {
+  /* 模态框背景 */
+  .ant-modal-content {
+    background-color: #1f1f1f;
+  }
+
+  /* 模态框标题 */
+  .ant-modal-header {
+    background-color: #1f1f1f;
+    border-bottom: 1px solid #303030;
+  }
+
+  .ant-modal-title {
+    color: #e6e6e6;
+  }
+
+  /* 模态框内容 */
+  .ant-modal-body {
+    color: #e6e6e6;
+  }
+
+  /* 模态框底部 */
+  .ant-modal-footer {
+    border-top: 1px solid #303030;
+    background-color: #1f1f1f;
+  }
+
+  /* 关闭按钮 */
+  .ant-modal-close-x {
+    color: #e6e6e6;
+  }
+}
+
+/* 令牌容器暗夜模式样式 */
+.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 {
+    background-color: rgba(255, 77, 79, 0.1);
+    border-color: rgba(255, 77, 79, 0.3);
+
+    p {
+      color: #ff7875;
+    }
+  }
+}
+
+/* 媒体查询方式添加暗夜模式支持 */
+@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>

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

@@ -0,0 +1,192 @@
+<script setup lang="ts">
+import type { RestoreOptions } from '@/api/backup'
+import type { UploadFile } from 'ant-design-vue'
+import backup from '@/api/backup'
+import { InboxOutlined } from '@ant-design/icons-vue'
+import { message, Modal } from 'ant-design-vue'
+import { reactive, ref } from 'vue'
+
+// Use UploadFile from ant-design-vue
+const uploadFiles = ref<UploadFile[]>([])
+const isRestoring = ref(false)
+
+const formModel = reactive({
+  securityToken: '',
+  restoreNginx: true,
+  restoreNginxUI: true,
+  verifyHash: true,
+})
+
+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)
+
+    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'))
+
+      // Show warning modal about restart
+      Modal.warning({
+        title: $gettext('Automatic Restart'),
+        content: $gettext('Nginx UI configuration has been restored and will restart automatically in a few seconds.'),
+        okText: $gettext('OK'),
+        maskClosable: false,
+      })
+    }
+
+    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)
+  }
+  finally {
+    isRestoring.value = false
+  }
+}
+</script>
+
+<template>
+  <ACard :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>
+
+      <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>
+
+      <AFormItem>
+        <AButton type="primary" :loading="isRestoring" @click="doRestore">
+          {{ $gettext('Start Restore') }}
+        </AButton>
+      </AFormItem>
+    </AForm>
+  </ACard>
+</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, filePath)
+	}
+
+	// Decrypt file content
+	decryptedData, err := AESDecrypt(encryptedData, key, iv)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrDecryptFile, filePath)
+	}
+
+	// Write decrypted content back
+	if err := os.WriteFile(filePath, decryptedData, 0644); err != nil {
+		return cosy.WrapErrorWithParams(ErrWriteDecryptedFile, filePath)
+	}
+
+	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")
+)

+ 369 - 0
internal/backup/restore.go

@@ -0,0 +1,369 @@
+package backup
+
+import (
+	"archive/zip"
+	"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, HashInfoFile)
+	}
+
+	// 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, nginxUIDir)
+	}
+
+	if err := os.MkdirAll(nginxDir, 0755); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrCreateDir, nginxDir)
+	}
+
+	// Extract nginx-ui.zip to nginx-ui directory
+	if err := extractZipArchive(nginxUIZipPath, nginxUIDir); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, "nginx-ui.zip")
+	}
+
+	// Extract nginx.zip to nginx directory
+	if err := extractZipArchive(nginxZipPath, nginxDir); err != nil {
+		return RestoreResult{}, cosy.WrapErrorWithParams(ErrExtractArchive, "nginx.zip")
+	}
+
+	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, err.Error())
+	}
+	defer reader.Close()
+
+	for _, file := range reader.File {
+		err := extractZipFile(file, destDir)
+		if err != nil {
+			return 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, file.Name)
+	}
+
+	// Create directory path if needed
+	filePath := filepath.Join(destDir, file.Name)
+
+	// Ensure the resulting file path is within the destination directory
+	destDirAbs, err := filepath.Abs(destDir)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrInvalidFilePath, "cannot resolve destination path")
+	}
+
+	filePathAbs, err := filepath.Abs(filePath)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrInvalidFilePath, file.Name)
+	}
+
+	if !strings.HasPrefix(filePathAbs, destDirAbs+string(os.PathSeparator)) {
+		return cosy.WrapErrorWithParams(ErrInvalidFilePath, file.Name)
+	}
+
+	if file.FileInfo().IsDir() {
+		if err := os.MkdirAll(filePath, file.Mode()); err != nil {
+			return cosy.WrapErrorWithParams(ErrCreateDir, filePath)
+		}
+		return nil
+	}
+
+	// Create parent directory if needed
+	if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil {
+		return cosy.WrapErrorWithParams(ErrCreateParentDir, filePath)
+	}
+
+	// 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, file.Name)
+		}
+		defer srcFile.Close()
+
+		// Read the link target
+		linkTargetBytes, err := io.ReadAll(srcFile)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrReadSymlink, file.Name)
+		}
+		linkTarget := string(linkTargetBytes)
+
+		// Verify the link target doesn't escape the destination directory
+		absLinkTarget := filepath.Clean(filepath.Join(filepath.Dir(filePath), linkTarget))
+		if !strings.HasPrefix(absLinkTarget, destDirAbs+string(os.PathSeparator)) {
+			return cosy.WrapErrorWithParams(ErrInvalidFilePath, linkTarget)
+		}
+
+		// Remove any existing file/link at the target path
+		_ = os.Remove(filePath)
+
+		// Create the symlink
+		if err := os.Symlink(linkTarget, filePath); err != nil {
+			return cosy.WrapErrorWithParams(ErrCreateSymlink, file.Name)
+		}
+
+		// Verify the resolved symlink path is within destination directory
+		resolvedPath, err := filepath.EvalSymlinks(filePath)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrEvalSymlinks, filePath)
+		}
+
+		resolvedPathAbs, err := filepath.Abs(resolvedPath)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrInvalidFilePath, resolvedPath)
+		}
+
+		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, resolvedPath)
+		}
+
+		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, filePath)
+	}
+	defer destFile.Close()
+
+	// Open source file in zip
+	srcFile, err := file.Open()
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrOpenZipEntry, file.Name)
+	}
+	defer srcFile.Close()
+
+	// Copy content
+	if _, err := io.Copy(destFile, srcFile); err != nil {
+		return cosy.WrapErrorWithParams(ErrCopyContent, file.Name)
+	}
+
+	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
+	}
+
+	// Remove all contents in the destination directory first
+	// Read directory entries
+	entries, err := os.ReadDir(destDir)
+	if err != nil {
+		return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "failed to read directory: "+err.Error())
+	}
+
+	// Remove each entry
+	for _, entry := range entries {
+		entryPath := filepath.Join(destDir, entry.Name())
+		err := os.RemoveAll(entryPath)
+		if err != nil {
+			return cosy.WrapErrorWithParams(ErrCopyNginxConfigDir, "failed to remove: "+err.Error())
+		}
+	}
+
+	// Copy files from backup to nginx config directory
+	if err := copyDirectory(nginxBackupDir, destDir); 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")
+}

+ 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 {

+ 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 (

Деякі файли не було показано, через те що забагато файлів було змінено