Ver código fonte

Merge pull request #451 from 0xJacky/refactor/config

refactor: config management
Jacky 9 meses atrás
pai
commit
26827b5114
64 arquivos alterados com 3373 adições e 1189 exclusões
  1. 90 49
      api/config/add.go
  2. 13 0
      api/config/base_path.go
  3. 30 10
      api/config/get.go
  4. 6 0
      api/config/list.go
  5. 37 0
      api/config/mkdir.go
  6. 99 8
      api/config/modify.go
  7. 97 0
      api/config/rename.go
  8. 12 1
      api/config/router.go
  9. 7 0
      api/user/otp.go
  10. 7 1
      api/user/router.go
  11. 27 1
      app/src/api/config.ts
  12. 3 0
      app/src/api/otp.ts
  13. 15 9
      app/src/components/Breadcrumb/Breadcrumb.vue
  14. 9 0
      app/src/components/Breadcrumb/types.d.ts
  15. 18 0
      app/src/components/Notification/cert.ts
  16. 37 0
      app/src/components/Notification/config.ts
  17. 15 19
      app/src/components/Notification/detailRender.ts
  18. 58 55
      app/src/components/OTP/useOTPModal.ts
  19. 2 2
      app/src/components/StdDesign/StdDataDisplay/StdCurd.vue
  20. 1 0
      app/src/components/StdDesign/StdDataDisplay/StdPagination.vue
  21. 0 1
      app/src/components/StdDesign/StdDataDisplay/StdTable.vue
  22. 5 0
      app/src/composables/useBreadcrumbs.ts
  23. 132 47
      app/src/language/en/app.po
  24. 132 47
      app/src/language/es/app.po
  25. 131 47
      app/src/language/fr_FR/app.po
  26. 133 50
      app/src/language/ko_KR/app.po
  27. 119 44
      app/src/language/messages.pot
  28. 132 50
      app/src/language/ru_RU/app.po
  29. 132 50
      app/src/language/vi_VN/app.po
  30. BIN
      app/src/language/zh_CN/app.mo
  31. 121 50
      app/src/language/zh_CN/app.po
  32. 132 47
      app/src/language/zh_TW/app.po
  33. 4 0
      app/src/layouts/BaseLayout.vue
  34. 15 2
      app/src/lib/http/index.ts
  35. 1 0
      app/src/pinia/moudule/user.ts
  36. 13 2
      app/src/routes/index.ts
  37. 1 1
      app/src/version.json
  38. 0 80
      app/src/views/config/Config.vue
  39. 0 160
      app/src/views/config/ConfigEdit.vue
  40. 345 0
      app/src/views/config/ConfigEditor.vue
  41. 181 0
      app/src/views/config/ConfigList.vue
  42. 74 0
      app/src/views/config/components/Mkdir.vue
  43. 93 0
      app/src/views/config/components/Rename.vue
  44. 4 0
      app/src/views/config/configColumns.ts
  45. 4 36
      app/src/views/notification/Notification.vue
  46. 58 0
      app/src/views/notification/notificationColumns.tsx
  47. 22 22
      app/src/views/pty/Terminal.vue
  48. 1 1
      app/version.json
  49. 1 1
      go.mod
  50. 1 1
      internal/config/config.go
  51. 257 0
      internal/config/sync.go
  52. 2 0
      internal/helper/directory_test.go
  53. 2 2
      internal/middleware/ip_whitelist.go
  54. 8 44
      internal/middleware/middleware.go
  55. 2 2
      internal/middleware/proxy.go
  56. 2 2
      internal/middleware/proxy_ws.go
  57. 43 0
      internal/middleware/secure_session.go
  58. 9 0
      model/config.go
  59. 1 0
      model/model.go
  60. 28 20
      query/certs.gen.go
  61. 370 0
      query/configs.gen.go
  62. 8 0
      query/gen.go
  63. 0 154
      router/operation_sync.go
  64. 71 71
      router/routers.go

+ 90 - 49
api/config/add.go

@@ -1,56 +1,97 @@
 package config
 
 import (
-	"github.com/0xJacky/Nginx-UI/api"
-	"github.com/0xJacky/Nginx-UI/internal/config"
-	"github.com/0xJacky/Nginx-UI/internal/nginx"
-	"github.com/gin-gonic/gin"
-	"net/http"
-	"os"
+    "github.com/0xJacky/Nginx-UI/api"
+    "github.com/0xJacky/Nginx-UI/internal/config"
+    "github.com/0xJacky/Nginx-UI/internal/helper"
+    "github.com/0xJacky/Nginx-UI/internal/nginx"
+    "github.com/0xJacky/Nginx-UI/model"
+    "github.com/0xJacky/Nginx-UI/query"
+    "github.com/gin-gonic/gin"
+    "github.com/sashabaranov/go-openai"
+    "net/http"
+    "os"
+    "path/filepath"
+    "time"
 )
 
 func AddConfig(c *gin.Context) {
-	var request struct {
-		Name    string `json:"name" binding:"required"`
-		Content string `json:"content" binding:"required"`
-	}
-
-	err := c.BindJSON(&request)
-	if err != nil {
-		api.ErrHandler(c, err)
-		return
-	}
-
-	name := request.Name
-	content := request.Content
-
-	path := nginx.GetConfPath("/", name)
-
-	if _, err = os.Stat(path); err == nil {
-		c.JSON(http.StatusNotAcceptable, gin.H{
-			"message": "config exist",
-		})
-		return
-	}
-
-	if content != "" {
-		err = os.WriteFile(path, []byte(content), 0644)
-		if err != nil {
-			api.ErrHandler(c, err)
-			return
-		}
-	}
-
-	output := nginx.Reload()
-	if nginx.GetLogLevel(output) >= nginx.Warn {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": output,
-		})
-		return
-	}
-
-	c.JSON(http.StatusOK, config.Config{
-		Name:    name,
-		Content: content,
-	})
+    var json struct {
+        Name        string `json:"name" binding:"required"`
+        NewFilepath string `json:"new_filepath" binding:"required"`
+        Content     string `json:"content"`
+        Overwrite   bool   `json:"overwrite"`
+        SyncNodeIds []int  `json:"sync_node_ids"`
+    }
+
+    if !api.BindAndValid(c, &json) {
+        return
+    }
+
+    name := json.Name
+    content := json.Content
+    path := json.NewFilepath
+    if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
+        c.JSON(http.StatusForbidden, gin.H{
+            "message": "new filepath is not under the nginx conf path",
+        })
+        return
+    }
+
+    if !json.Overwrite && helper.FileExists(path) {
+        c.JSON(http.StatusNotAcceptable, gin.H{
+            "message": "File exists",
+        })
+        return
+    }
+
+    // check if the dir exists, if not, use mkdirAll to create the dir
+    dir := filepath.Dir(path)
+    if !helper.FileExists(dir) {
+        err := os.MkdirAll(dir, 0755)
+        if err != nil {
+            api.ErrHandler(c, err)
+            return
+        }
+    }
+
+    err := os.WriteFile(path, []byte(content), 0644)
+    if err != nil {
+        api.ErrHandler(c, err)
+        return
+    }
+
+    output := nginx.Reload()
+    if nginx.GetLogLevel(output) >= nginx.Warn {
+        c.JSON(http.StatusInternalServerError, gin.H{
+            "message": output,
+        })
+        return
+    }
+
+    q := query.Config
+    _, err = q.Where(q.Filepath.Eq(path)).Delete()
+    if err != nil {
+        api.ErrHandler(c, err)
+        return
+    }
+
+    err = q.Create(&model.Config{
+        Name:          name,
+        Filepath:      path,
+        SyncNodeIds:   json.SyncNodeIds,
+        SyncOverwrite: json.Overwrite,
+    })
+    if err != nil {
+        api.ErrHandler(c, err)
+        return
+    }
+
+    c.JSON(http.StatusOK, config.Config{
+        Name:            name,
+        Content:         content,
+        ChatGPTMessages: make([]openai.ChatCompletionMessage, 0),
+        FilePath:        path,
+        ModifiedAt:      time.Now(),
+    })
 }

+ 13 - 0
api/config/base_path.go

@@ -0,0 +1,13 @@
+package config
+
+import (
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func GetBasePath(c *gin.Context) {
+	c.JSON(http.StatusOK, gin.H{
+		"base_path": nginx.GetConfPath(),
+	})
+}

+ 30 - 10
api/config/get.go

@@ -3,6 +3,7 @@ package config
 import (
 	"github.com/0xJacky/Nginx-UI/api"
 	"github.com/0xJacky/Nginx-UI/internal/config"
+	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
 	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
@@ -11,28 +12,37 @@ import (
 	"os"
 )
 
+type APIConfigResp struct {
+	config.Config
+	SyncNodeIds   []int `json:"sync_node_ids" gorm:"serializer:json"`
+	SyncOverwrite bool  `json:"sync_overwrite"`
+}
+
 func GetConfig(c *gin.Context) {
 	name := c.Param("name")
 
 	path := nginx.GetConfPath("/", name)
+	if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
+		c.JSON(http.StatusForbidden, gin.H{
+			"message": "path is not under the nginx conf path",
+		})
+		return
+	}
 
 	stat, err := os.Stat(path)
-
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
 	}
 
 	content, err := os.ReadFile(path)
-
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
 	}
-
+	q := query.Config
 	g := query.ChatGPTLog
 	chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
-
 	if err != nil {
 		api.ErrHandler(c, err)
 		return
@@ -42,11 +52,21 @@ func GetConfig(c *gin.Context) {
 		chatgpt.Content = make([]openai.ChatCompletionMessage, 0)
 	}
 
-	c.JSON(http.StatusOK, config.Config{
-		Name:            stat.Name(),
-		Content:         string(content),
-		ChatGPTMessages: chatgpt.Content,
-		FilePath:        path,
-		ModifiedAt:      stat.ModTime(),
+	cfg, err := q.Where(q.Filepath.Eq(path)).FirstOrInit()
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, APIConfigResp{
+		Config: config.Config{
+			Name:            stat.Name(),
+			Content:         string(content),
+			ChatGPTMessages: chatgpt.Content,
+			FilePath:        path,
+			ModifiedAt:      stat.ModTime(),
+		},
+		SyncNodeIds:   cfg.SyncNodeIds,
+		SyncOverwrite: cfg.SyncOverwrite,
 	})
 }

+ 6 - 0
api/config/list.go

@@ -8,9 +8,11 @@ import (
 	"github.com/gin-gonic/gin"
 	"net/http"
 	"os"
+	"strings"
 )
 
 func GetConfigs(c *gin.Context) {
+	name := c.Query("name")
 	orderBy := c.Query("order_by")
 	sort := c.DefaultQuery("sort", "desc")
 	dir := c.DefaultQuery("dir", "/")
@@ -28,6 +30,10 @@ func GetConfigs(c *gin.Context) {
 		file := configFiles[i]
 		fileInfo, _ := file.Info()
 
+		if name != "" && !strings.Contains(file.Name(), name) {
+			continue
+		}
+
 		switch mode := fileInfo.Mode(); {
 		case mode.IsRegular(): // regular file, not a hidden file
 			if "." == file.Name()[0:1] {

+ 37 - 0
api/config/mkdir.go

@@ -0,0 +1,37 @@
+package config
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"os"
+)
+
+func Mkdir(c *gin.Context) {
+	var json struct {
+		BasePath   string `json:"base_path"`
+		FolderName string `json:"folder_name"`
+	}
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+	fullPath := nginx.GetConfPath(json.BasePath, json.FolderName)
+	if !helper.IsUnderDirectory(fullPath, nginx.GetConfPath()) {
+		c.JSON(http.StatusForbidden, gin.H{
+			"message": "You are not allowed to create a folder " +
+				"outside of the nginx configuration directory",
+		})
+		return
+	}
+	err := os.Mkdir(fullPath, 0755)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+}

+ 99 - 8
api/config/modify.go

@@ -2,10 +2,16 @@ package config
 
 import (
 	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/config"
+	"github.com/0xJacky/Nginx-UI/internal/helper"
 	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
 	"github.com/gin-gonic/gin"
+	"github.com/sashabaranov/go-openai"
 	"net/http"
 	"os"
+	"time"
 )
 
 type EditConfigJson struct {
@@ -14,15 +20,41 @@ type EditConfigJson struct {
 
 func EditConfig(c *gin.Context) {
 	name := c.Param("name")
-	var request EditConfigJson
-	err := c.BindJSON(&request)
-	if err != nil {
-		api.ErrHandler(c, err)
+	var json struct {
+		Name        string `json:"name" binding:"required"`
+		Filepath    string `json:"filepath" binding:"required"`
+		NewFilepath string `json:"new_filepath" binding:"required"`
+		Content     string `json:"content"`
+		Overwrite   bool   `json:"overwrite"`
+		SyncNodeIds []int  `json:"sync_node_ids"`
+	}
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+
+	path := json.Filepath
+	if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
+		c.JSON(http.StatusForbidden, gin.H{
+			"message": "filepath is not under the nginx conf path",
+		})
+		return
+	}
+
+	if !helper.IsUnderDirectory(json.NewFilepath, nginx.GetConfPath()) {
+		c.JSON(http.StatusForbidden, gin.H{
+			"message": "new filepath is not under the nginx conf path",
+		})
+		return
+	}
+
+	if _, err := os.Stat(path); os.IsNotExist(err) {
+		c.JSON(http.StatusNotFound, gin.H{
+			"message": "file not found",
+		})
 		return
 	}
-	path := nginx.GetConfPath("/", name)
-	content := request.Content
 
+	content := json.Content
 	origContent, err := os.ReadFile(path)
 	if err != nil {
 		api.ErrHandler(c, err)
@@ -37,8 +69,51 @@ func EditConfig(c *gin.Context) {
 		}
 	}
 
-	output := nginx.Reload()
+	q := query.Config
+	cfg, err := q.Where(q.Filepath.Eq(json.Filepath)).FirstOrCreate()
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	_, err = q.Where(q.Filepath.Eq(json.Filepath)).Updates(&model.Config{
+		Name:          json.Name,
+		Filepath:      json.NewFilepath,
+		SyncNodeIds:   json.SyncNodeIds,
+		SyncOverwrite: json.Overwrite,
+	})
 
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	g := query.ChatGPTLog
+	// handle rename
+	if path != json.NewFilepath {
+		if helper.FileExists(json.NewFilepath) {
+			c.JSON(http.StatusNotAcceptable, gin.H{
+				"message": "File exists",
+			})
+			return
+		}
+		err := os.Rename(json.Filepath, json.NewFilepath)
+		if err != nil {
+			api.ErrHandler(c, err)
+			return
+		}
+
+		// update ChatGPT record
+		_, _ = g.Where(g.Name.Eq(json.NewFilepath)).Delete()
+		_, _ = g.Where(g.Name.Eq(path)).Update(g.Name, json.NewFilepath)
+	}
+
+	err = config.SyncToRemoteServer(cfg, json.NewFilepath)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	output := nginx.Reload()
 	if nginx.GetLogLevel(output) >= nginx.Warn {
 		c.JSON(http.StatusInternalServerError, gin.H{
 			"message": output,
@@ -46,5 +121,21 @@ func EditConfig(c *gin.Context) {
 		return
 	}
 
-	GetConfig(c)
+	chatgpt, err := g.Where(g.Name.Eq(json.NewFilepath)).FirstOrCreate()
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	if chatgpt.Content == nil {
+		chatgpt.Content = make([]openai.ChatCompletionMessage, 0)
+	}
+
+	c.JSON(http.StatusOK, config.Config{
+		Name:            name,
+		Content:         content,
+		ChatGPTMessages: chatgpt.Content,
+		FilePath:        json.NewFilepath,
+		ModifiedAt:      time.Now(),
+	})
 }

+ 97 - 0
api/config/rename.go

@@ -0,0 +1,97 @@
+package config
+
+import (
+	"github.com/0xJacky/Nginx-UI/api"
+	"github.com/0xJacky/Nginx-UI/internal/config"
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/logger"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"net/http"
+	"os"
+)
+
+func Rename(c *gin.Context) {
+	var json struct {
+		BasePath    string `json:"base_path"`
+		OrigName    string `json:"orig_name"`
+		NewName     string `json:"new_name"`
+		SyncNodeIds []int  `json:"sync_node_ids" gorm:"serializer:json"`
+	}
+	if !api.BindAndValid(c, &json) {
+		return
+	}
+	logger.Debug(json)
+	if json.OrigName == json.NewName {
+		c.JSON(http.StatusOK, gin.H{
+			"message": "ok",
+		})
+		return
+	}
+	origFullPath := nginx.GetConfPath(json.BasePath, json.OrigName)
+	newFullPath := nginx.GetConfPath(json.BasePath, json.NewName)
+	if !helper.IsUnderDirectory(origFullPath, nginx.GetConfPath()) ||
+		!helper.IsUnderDirectory(newFullPath, nginx.GetConfPath()) {
+		c.JSON(http.StatusForbidden, gin.H{
+			"message": "you are not allowed to rename a file " +
+				"outside of the nginx config path",
+		})
+		return
+	}
+
+	stat, err := os.Stat(origFullPath)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	if helper.FileExists(newFullPath) {
+		c.JSON(http.StatusNotAcceptable, gin.H{
+			"message": "target file already exists",
+		})
+		return
+	}
+
+	err = os.Rename(origFullPath, newFullPath)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	// update ChatGPT records
+	g := query.ChatGPTLog
+	q := query.Config
+	cfg, err := q.Where(q.Filepath.Eq(origFullPath)).FirstOrInit()
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+	if !stat.IsDir() {
+		_, _ = g.Where(g.Name.Eq(newFullPath)).Delete()
+		_, _ = g.Where(g.Name.Eq(origFullPath)).Update(g.Name, newFullPath)
+		// for file, the sync policy for this file is used
+		json.SyncNodeIds = cfg.SyncNodeIds
+	} else {
+		// is directory, update all records under the directory
+		_, _ = g.Where(g.Name.Like(origFullPath+"%")).Update(g.Name, g.Name.Replace(origFullPath, newFullPath))
+	}
+
+	_, err = q.Where(q.Filepath.Eq(origFullPath)).Update(q.Filepath, newFullPath)
+	if err != nil {
+		api.ErrHandler(c, err)
+		return
+	}
+
+	if len(json.SyncNodeIds) > 0 {
+		err = config.SyncRenameOnRemoteServer(origFullPath, newFullPath, json.SyncNodeIds)
+		if err != nil {
+			api.ErrHandler(c, err)
+			return
+		}
+	}
+
+	c.JSON(http.StatusOK, gin.H{
+		"message": "ok",
+	})
+}

+ 12 - 1
api/config/router.go

@@ -1,10 +1,21 @@
 package config
 
-import "github.com/gin-gonic/gin"
+import (
+	"github.com/0xJacky/Nginx-UI/internal/middleware"
+	"github.com/gin-gonic/gin"
+)
 
 func InitRouter(r *gin.RouterGroup) {
+	r.GET("config_base_path", GetBasePath)
+
 	r.GET("configs", GetConfigs)
 	r.GET("config/*name", GetConfig)
 	r.POST("config", AddConfig)
 	r.POST("config/*name", EditConfig)
+
+	o := r.Group("", middleware.RequireSecureSession())
+	{
+		o.POST("config_mkdir", Mkdir)
+		o.POST("config_rename", Rename)
+	}
 }

+ 7 - 0
api/user/otp.go

@@ -170,6 +170,13 @@ func OTPStatus(c *gin.Context) {
 	})
 }
 
+func SecureSessionStatus(c *gin.Context) {
+	// if you can visit this endpoint, you are already in a secure session
+	c.JSON(http.StatusOK, gin.H{
+		"status": true,
+	})
+}
+
 func StartSecure2FASession(c *gin.Context) {
 	var json struct {
 		OTP          string `json:"otp"`

+ 7 - 1
api/user/router.go

@@ -1,6 +1,9 @@
 package user
 
-import "github.com/gin-gonic/gin"
+import (
+    "github.com/0xJacky/Nginx-UI/internal/middleware"
+    "github.com/gin-gonic/gin"
+)
 
 func InitAuthRouter(r *gin.RouterGroup) {
 	r.POST("/login", Login)
@@ -23,5 +26,8 @@ func InitUserRouter(r *gin.RouterGroup) {
 	r.GET("/otp_secret", GenerateTOTP)
 	r.POST("/otp_enroll", EnrollTOTP)
 	r.POST("/otp_reset", ResetOTP)
+
+    r.GET("/otp_secure_session_status",
+        middleware.RequireSecureSession(), SecureSessionStatus)
 	r.POST("/otp_secure_session", StartSecure2FASession)
 }

+ 27 - 1
app/src/api/config.ts

@@ -1,5 +1,6 @@
 import Curd from '@/api/curd'
 import type { ChatComplicationMessage } from '@/api/openai'
+import http from '@/lib/http'
 
 export interface Config {
   name: string
@@ -7,8 +8,33 @@ export interface Config {
   chatgpt_messages: ChatComplicationMessage[]
   filepath: string
   modified_at: string
+  sync_node_ids?: number[]
+  sync_overwrite?: false
 }
 
-const config: Curd<Config> = new Curd('/config')
+class ConfigCurd extends Curd<Config> {
+  constructor() {
+    super('/config')
+  }
+
+  get_base_path() {
+    return http.get('/config_base_path')
+  }
+
+  mkdir(basePath: string, name: string) {
+    return http.post('/config_mkdir', { base_path: basePath, folder_name: name })
+  }
+
+  rename(basePath: string, origName: string, newName: string, syncNodeIds: number[]) {
+    return http.post('/config_rename', {
+      base_path: basePath,
+      orig_name: origName,
+      new_name: newName,
+      sync_node_ids: syncNodeIds,
+    })
+  }
+}
+
+const config: ConfigCurd = new ConfigCurd()
 
 export default config

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

@@ -24,6 +24,9 @@ const otp = {
       recovery_code,
     })
   },
+  secure_session_status() {
+    return http.get('/otp_secure_session_status')
+  },
 }
 
 export default otp

+ 15 - 9
app/src/components/Breadcrumb/Breadcrumb.vue

@@ -1,17 +1,13 @@
 <script setup lang="ts">
-interface bread {
-  name: string
-  translatedName: () => string
-  path: string
-  hasChildren?: boolean
-}
+import type { Bread } from '@/components/Breadcrumb/types'
+import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
 
 const name = ref()
 const route = useRoute()
 const router = useRouter()
 
-const breadList = computed(() => {
-  const result: bread[] = []
+const computedBreadList = computed(() => {
+  const result: Bread[] = []
 
   name.value = route.meta.name
 
@@ -36,6 +32,16 @@ const breadList = computed(() => {
 
   return result
 })
+
+const breadList = useBreadcrumbs()
+
+onMounted(() => {
+  breadList.value = computedBreadList.value
+})
+
+watch(route, () => {
+  breadList.value = computedBreadList.value
+})
 </script>
 
 <template>
@@ -46,7 +52,7 @@ const breadList = computed(() => {
     >
       <RouterLink
         v-if="index === 0 || !item.hasChildren && index !== breadList.length - 1"
-        :to="{ path: item.path === '' ? '/' : item.path }"
+        :to="{ path: item.path === '' ? '/' : item.path, query: item.query }"
       >
         {{ item.translatedName() }}
       </RouterLink>

+ 9 - 0
app/src/components/Breadcrumb/types.d.ts

@@ -0,0 +1,9 @@
+import {LocationQueryRaw} from "vue-router";
+
+export interface Bread {
+  name: string
+  translatedName: () => string
+  path?: string
+  query?: LocationQueryRaw
+  hasChildren?: boolean
+}

+ 18 - 0
app/src/components/Notification/cert.ts

@@ -0,0 +1,18 @@
+export function syncCertificateSuccess(text: string) {
+  const data = JSON.parse(text)
+
+  return $gettext('Sync Certificate %{cert_name} to %{env_name} successfully',
+    { cert_name: data.cert_name, env_name: data.env_name })
+}
+
+export function syncCertificateError(text: string) {
+  const data = JSON.parse(text)
+
+  if (data.status_code === 404) {
+    return $gettext('Sync Certificate %{cert_name} to %{env_name} failed, please upgrade the remote Nginx UI to the latest version',
+      { cert_name: data.cert_name, env_name: data.env_name }, true)
+  }
+
+  return $gettext('Sync Certificate %{cert_name} to %{env_name} failed, response: %{resp}',
+    { cert_name: data.cert_name, env_name: data.env_name, resp: data.resp_body }, true)
+}

+ 37 - 0
app/src/components/Notification/config.ts

@@ -0,0 +1,37 @@
+export function syncConfigSuccess(text: string) {
+  const data = JSON.parse(text)
+
+  return $gettext('Sync Config %{config_name} to %{env_name} successfully',
+    { config_name: data.config_name, env_name: data.env_name })
+}
+
+export function syncConfigError(text: string) {
+  const data = JSON.parse(text)
+
+  if (data.status_code === 404) {
+    return $gettext('Sync config %{cert_name} to %{env_name} failed, please upgrade the remote Nginx UI to the latest version',
+      { config_name: data.config_name, env_name: data.env_name }, true)
+  }
+
+  return $gettext('Sync config %{config_name} to %{env_name} failed, response: %{resp}',
+    { cert_name: data.cert_name, env_name: data.env_name, resp: data.resp_body }, true)
+}
+
+export function syncRenameConfigSuccess(text: string) {
+  const data = JSON.parse(text)
+
+  return $gettext('Rename %{orig_path} to %{new_path} on %{env_name} successfully',
+    { orig_path: data.orig_path, new_path: data.orig_path, env_name: data.env_name })
+}
+
+export function syncRenameConfigError(text: string) {
+  const data = JSON.parse(text)
+
+  if (data.status_code === 404) {
+    return $gettext('Rename %{orig_path} to %{new_path} on %{env_name} failed, please upgrade the remote Nginx UI to the latest version',
+      { orig_path: data.orig_path, new_path: data.orig_path, env_name: data.env_name }, true)
+  }
+
+  return $gettext('Rename %{orig_path} to %{new_path} on %{env_name} failed, response: %{resp}',
+    { orig_path: data.orig_path, new_path: data.orig_path, resp: data.resp_body, env_name: data.env_name }, true)
+}

+ 15 - 19
app/src/components/Notification/detailRender.ts

@@ -1,4 +1,11 @@
 import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
+import { syncCertificateError, syncCertificateSuccess } from '@/components/Notification/cert'
+import {
+  syncConfigError,
+  syncConfigSuccess,
+  syncRenameConfigError,
+  syncRenameConfigSuccess,
+} from '@/components/Notification/config'
 
 export const detailRender = (args: customRender) => {
   switch (args.record.title) {
@@ -6,26 +13,15 @@ export const detailRender = (args: customRender) => {
       return syncCertificateSuccess(args.text)
     case 'Sync Certificate Error':
       return syncCertificateError(args.text)
+    case 'Rename Remote Config Success':
+      return syncRenameConfigSuccess(args.text)
+    case 'Rename Remote Config Error':
+      return syncRenameConfigError(args.text)
+    case 'Sync Config Success':
+      return syncConfigSuccess(args.text)
+    case 'Sync Config Error':
+      return syncConfigError(args.text)
     default:
       return args.text
   }
 }
-
-function syncCertificateSuccess(text: string) {
-  const data = JSON.parse(text)
-
-  return $gettext('Sync Certificate %{cert_name} to %{env_name} successfully',
-    { cert_name: data.cert_name, env_name: data.env_name })
-}
-
-function syncCertificateError(text: string) {
-  const data = JSON.parse(text)
-
-  if (data.status_code === 404) {
-    return $gettext('Sync Certificate %{cert_name} to %{env_name} failed, please upgrade the remote Nginx UI to the latest version',
-      { cert_name: data.cert_name, env_name: data.env_name }, true)
-  }
-
-  return $gettext('Sync Certificate %{cert_name} to %{env_name} failed, response: %{resp}',
-    { cert_name: data.cert_name, env_name: data.env_name, resp: data.resp_body }, true)
-}

+ 58 - 55
app/src/components/OTP/useOTPModal.ts

@@ -3,15 +3,12 @@ import { Modal, message } from 'ant-design-vue'
 import { useCookies } from '@vueuse/integrations/useCookies'
 import OTPAuthorization from '@/components/OTP/OTPAuthorization.vue'
 import otp from '@/api/otp'
-
-export interface OTPModalProps {
-  onOk?: (secureSessionId: string) => void
-  onCancel?: () => void
-}
+import { useUserStore } from '@/pinia'
 
 const useOTPModal = () => {
   const refOTPAuthorization = ref<typeof OTPAuthorization>()
   const randomId = Math.random().toString(36).substring(2, 8)
+  const { secureSessionId } = storeToRefs(useUserStore())
 
   const injectStyles = () => {
     const style = document.createElement('style')
@@ -24,66 +21,72 @@ const useOTPModal = () => {
     document.head.appendChild(style)
   }
 
-  const open = async ({ onOk, onCancel }: OTPModalProps) => {
+  const open = async (): Promise<string> => {
     const { status } = await otp.status()
-    if (!status) {
-      onOk?.('')
 
-      return
-    }
+    return new Promise((resolve, reject) => {
+      if (!status) {
+        resolve('')
 
-    const cookies = useCookies(['nginx-ui-2fa'])
-    const ssid = cookies.get('secure_session_id')
-    if (ssid) {
-      onOk?.(ssid)
+        return
+      }
 
-      return
-    }
+      const cookies = useCookies(['nginx-ui-2fa'])
+      const ssid = cookies.get('secure_session_id')
+      if (ssid) {
+        resolve(ssid)
+        secureSessionId.value = ssid
 
-    injectStyles()
-    let container: HTMLDivElement | null = document.createElement('div')
-    document.body.appendChild(container)
+        return
+      }
 
-    const close = () => {
-      render(null, container!)
-      document.body.removeChild(container!)
-      container = null
-    }
+      injectStyles()
+      let container: HTMLDivElement | null = document.createElement('div')
+      document.body.appendChild(container)
 
-    const verify = (passcode: string, recovery: string) => {
-      otp.start_secure_session(passcode, recovery).then(r => {
-        cookies.set('secure_session_id', r.session_id, { maxAge: 60 * 3 })
-        onOk?.(r.session_id)
-        close()
-      }).catch(async () => {
-        refOTPAuthorization.value?.clearInput()
-        await message.error($gettext('Invalid passcode or recovery code'))
-      })
-    }
+      const close = () => {
+        render(null, container!)
+        document.body.removeChild(container!)
+        container = null
+      }
 
-    const vnode = createVNode(Modal, {
-      open: true,
-      title: $gettext('Two-factor authentication required'),
-      centered: true,
-      maskClosable: false,
-      class: randomId,
-      footer: false,
-      onCancel: () => {
-        close()
-        onCancel?.()
-      },
-    }, {
-      default: () => h(
-        OTPAuthorization,
-        {
-          ref: refOTPAuthorization,
-          class: 'mt-3',
-          onOnSubmit: verify,
+      const verify = (passcode: string, recovery: string) => {
+        otp.start_secure_session(passcode, recovery).then(r => {
+          cookies.set('secure_session_id', r.session_id, { maxAge: 60 * 3 })
+          resolve(r.session_id)
+          close()
+          secureSessionId.value = r.session_id
+        }).catch(async () => {
+          refOTPAuthorization.value?.clearInput()
+          await message.error($gettext('Invalid passcode or recovery code'))
+        })
+      }
+
+      const vnode = createVNode(Modal, {
+        open: true,
+        title: $gettext('Two-factor authentication required'),
+        centered: true,
+        maskClosable: false,
+        class: randomId,
+        footer: false,
+        onCancel: () => {
+          close()
+          // eslint-disable-next-line prefer-promise-reject-errors
+          reject()
         },
-      ),
-    })
+      }, {
+        default: () => h(
+          OTPAuthorization,
+          {
+            ref: refOTPAuthorization,
+            class: 'mt-3',
+            onOnSubmit: verify,
+          },
+        ),
+      })
 
-    render(vnode, container)
+      render(vnode, container!)
+    })
   }
 
   return { open }

+ 2 - 2
app/src/components/StdDesign/StdDataDisplay/StdCurd.vue

@@ -13,7 +13,7 @@ export interface StdCurdProps<T> extends StdTableProps<T> {
   modalMask?: boolean
   exportExcel?: boolean
   importExcel?: boolean
-
+  disableTrash?: boolean
   disableAdd?: boolean
   onClickAdd?: () => void
 
@@ -201,7 +201,7 @@ const localOverwriteParams = reactive(props.overwriteParams ?? {})
             @click="add"
           >{{ $gettext('Add') }}</a>
           <slot name="extra" />
-          <template v-if="!disableDelete">
+          <template v-if="!disableDelete && !disableTrash">
             <a
               v-if="!getParams.trash"
               @click="getParams.trash = true"

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

@@ -33,6 +33,7 @@ const pageSize = computed({
       v-model:pageSize="pageSize"
       :disabled="loading"
       :current="pagination.current_page"
+      show-size-changer
       :size="size"
       :total="pagination.total"
       @change="change"

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

@@ -20,7 +20,6 @@ export interface StdTableProps<T = any> {
   title?: string
   mode?: string
   rowKey?: string
-
   api: Curd<T>
   columns: Column[]
   // eslint-disable-next-line @typescript-eslint/no-explicit-any

+ 5 - 0
app/src/composables/useBreadcrumbs.ts

@@ -0,0 +1,5 @@
+import type { Bread } from '@/components/Breadcrumb/types'
+
+export const useBreadcrumbs = () => {
+  return inject('breadList') as Ref<Bread[]>
+}

+ 132 - 47
app/src/language/en/app.po

@@ -17,15 +17,15 @@ msgstr ""
 msgid "2FA Settings"
 msgstr ""
 
-#: src/routes/index.ts:266
+#: src/routes/index.ts:277
 msgid "About"
 msgstr "About"
 
-#: src/routes/index.ts:193 src/views/domain/ngx_conf/LogEntry.vue:76
+#: src/routes/index.ts:204 src/views/domain/ngx_conf/LogEntry.vue:76
 msgid "Access Logs"
 msgstr ""
 
-#: src/routes/index.ts:131 src/views/certificate/ACMEUser.vue:76
+#: src/routes/index.ts:142 src/views/certificate/ACMEUser.vue:76
 #: src/views/certificate/ACMEUserSelector.vue:85
 #, fuzzy
 msgid "ACME User"
@@ -33,8 +33,9 @@ msgstr "Username"
 
 #: src/views/certificate/ACMEUser.vue:59
 #: src/views/certificate/CertificateList/certColumns.tsx:89
-#: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34
-#: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131
+#: src/views/certificate/DNSCredential.vue:33
+#: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
+#: src/views/environment/envColumns.tsx:131
 #: src/views/notification/Notification.vue:37
 #: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
@@ -51,6 +52,12 @@ msgstr "Action"
 msgid "Add"
 msgstr ""
 
+#: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:128
+#: src/views/config/ConfigEditor.vue:187
+#, fuzzy
+msgid "Add Configuration"
+msgstr "Edit Configuration"
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:95
 msgid "Add Directive Below"
 msgstr "Add Directive Below"
@@ -187,9 +194,9 @@ msgid "Auto-renewal enabled for %{name}"
 msgstr "Auto-renewal enabled for %{name}"
 
 #: src/views/certificate/CertificateEditor.vue:247
-#: src/views/config/Config.vue:71 src/views/config/ConfigEdit.vue:87
-#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168
-#: src/views/stream/StreamEdit.vue:245
+#: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
+#: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
+#: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
 msgid "Back"
 msgstr "Back"
 
@@ -218,7 +225,7 @@ msgstr ""
 msgid "Base information"
 msgstr "Base information"
 
-#: src/views/config/ConfigEdit.vue:115
+#: src/views/config/ConfigEditor.vue:224
 #: src/views/domain/components/RightSettings.vue:75
 #: src/views/preference/Preference.vue:110
 #: src/views/stream/components/RightSettings.vue:74
@@ -280,13 +287,13 @@ msgid_plural "Certificates Status"
 msgstr[0] "Certificate Status"
 msgstr[1] "Certificate Status"
 
-#: src/routes/index.ts:122
+#: src/routes/index.ts:133
 #: src/views/certificate/CertificateList/Certificate.vue:13
 #, fuzzy
 msgid "Certificates"
 msgstr "Certificate Status"
 
-#: src/routes/index.ts:139
+#: src/routes/index.ts:150
 #, fuzzy
 msgid "Certificates List"
 msgstr "Certificate is valid"
@@ -308,6 +315,11 @@ msgid_plural "Changed Certificates"
 msgstr[0] "Certificate is valid"
 msgstr[1] "Certificate is valid"
 
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "Changed Path"
+msgstr "Certificate is valid"
+
 #: src/views/environment/BatchUpgrader.vue:161 src/views/system/Upgrade.vue:190
 msgid "Channel"
 msgstr ""
@@ -357,7 +369,7 @@ msgstr ""
 msgid "Configuration Name"
 msgstr "Configuration Name"
 
-#: src/views/config/Config.vue:42
+#: src/views/config/ConfigList.vue:91
 msgid "Configurations"
 msgstr "Configurations"
 
@@ -408,10 +420,25 @@ msgstr "Created at"
 msgid "Create Another"
 msgstr "Create Another"
 
+#: src/views/config/ConfigList.vue:109
+#, fuzzy
+msgid "Create File"
+msgstr "Created at"
+
+#: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
+#, fuzzy
+msgid "Create Folder"
+msgstr "Create Another"
+
 #: src/views/notification/Notification.vue:31 src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgstr "Created at"
 
+#: src/views/config/components/Mkdir.vue:35
+#, fuzzy
+msgid "Created successfully"
+msgstr "Disabled successfully"
+
 #: src/language/constants.ts:9
 msgid "Creating client facilitates communication with the CA server"
 msgstr ""
@@ -447,7 +474,8 @@ msgid ""
 "indicator."
 msgstr ""
 
-#: src/routes/index.ts:39
+#: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
+#: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
 msgid "Dashboard"
 msgstr "Dashboard"
 
@@ -528,7 +556,7 @@ msgstr "Directive"
 msgid "Directives"
 msgstr "Directives"
 
-#: src/views/config/config.ts:18
+#: src/views/config/configColumns.ts:22
 #, fuzzy
 msgid "Directory"
 msgstr "Directive"
@@ -560,7 +588,7 @@ msgstr "Disabled successfully"
 msgid "Disk IO"
 msgstr "Disk IO"
 
-#: src/routes/index.ts:167 src/views/certificate/DNSCredential.vue:40
+#: src/routes/index.ts:178 src/views/certificate/DNSCredential.vue:40
 msgid "DNS Credentials"
 msgstr ""
 
@@ -683,7 +711,7 @@ msgstr "Saved successfully"
 msgid "Edit %{n}"
 msgstr "Edit %{n}"
 
-#: src/routes/index.ts:112 src/views/config/ConfigEdit.vue:81
+#: src/routes/index.ts:122 src/views/config/ConfigEditor.vue:187
 msgid "Edit Configuration"
 msgstr "Edit Configuration"
 
@@ -775,7 +803,11 @@ msgstr "Enabled successfully"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Encrypt website with Let's Encrypt"
 
-#: src/routes/index.ts:217 src/views/environment/Environment.vue:34
+#: src/views/config/ConfigList.vue:151
+msgid "Enter"
+msgstr ""
+
+#: src/routes/index.ts:228 src/views/environment/Environment.vue:34
 msgid "Environment"
 msgstr ""
 
@@ -792,7 +824,7 @@ msgstr "Comments"
 msgid "Error"
 msgstr ""
 
-#: src/routes/index.ts:200 src/views/domain/ngx_conf/LogEntry.vue:84
+#: src/routes/index.ts:211 src/views/domain/ngx_conf/LogEntry.vue:84
 msgid "Error Logs"
 msgstr ""
 
@@ -841,7 +873,7 @@ msgstr ""
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr ""
 
-#: src/views/config/config.ts:20
+#: src/views/config/configColumns.ts:24
 msgid "File"
 msgstr ""
 
@@ -871,16 +903,16 @@ msgstr "Finished"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:90
+#: src/views/config/ConfigEditor.vue:199
 msgid "Format Code"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:68
+#: src/views/config/ConfigEditor.vue:166
 #, fuzzy
 msgid "Format error %{msg}"
 msgstr "Save error %{msg}"
 
-#: src/views/config/ConfigEdit.vue:66
+#: src/views/config/ConfigEditor.vue:164
 #, fuzzy
 msgid "Format successfully"
 msgstr "Saved successfully"
@@ -961,7 +993,7 @@ msgstr ""
 msgid "Import"
 msgstr ""
 
-#: src/routes/index.ts:157 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:168 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Import Certificate"
 msgstr "Certificate Status"
@@ -991,7 +1023,7 @@ msgstr ""
 msgid "Input the recovery code:"
 msgstr ""
 
-#: src/routes/index.ts:288 src/views/other/Install.vue:134
+#: src/routes/index.ts:299 src/views/other/Install.vue:134
 msgid "Install"
 msgstr "Install"
 
@@ -1013,7 +1045,17 @@ msgstr "Invalid E-mail!"
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:60
+#: src/views/config/components/Rename.vue:62
+#: src/views/config/ConfigEditor.vue:233
+#, fuzzy
+msgid "Invalid filename"
+msgstr "Invalid E-mail!"
+
+#: src/views/config/components/Mkdir.vue:60
+msgid "Invalid folder name"
+msgstr ""
+
+#: src/components/OTP/useOTPModal.ts:64
 msgid "Invalid passcode or recovery code"
 msgstr ""
 
@@ -1118,7 +1160,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "Login"
 
-#: src/routes/index.ts:294 src/views/other/Login.vue:192
+#: src/routes/index.ts:305 src/views/other/Login.vue:192
 msgid "Login"
 msgstr "Login"
 
@@ -1153,7 +1195,8 @@ msgstr ""
 "Make sure you have configured a reverse proxy for .well-known directory to "
 "HTTPChallengePort (default: 9180) before getting the certificate."
 
-#: src/routes/index.ts:102
+#: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
+#: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
 msgid "Manage Configs"
 msgstr "Manage Configs"
 
@@ -1166,7 +1209,7 @@ msgstr "Manage Sites"
 msgid "Manage Streams"
 msgstr "Manage Sites"
 
-#: src/routes/index.ts:240 src/views/user/User.vue:9
+#: src/routes/index.ts:251 src/views/user/User.vue:9
 msgid "Manage Users"
 msgstr "Manage Users"
 
@@ -1200,11 +1243,12 @@ msgstr "Advance Mode"
 #: src/components/ChatGPT/ChatGPT.vue:248
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
+#: src/views/config/ConfigList.vue:151
 #, fuzzy
 msgid "Modify"
 msgstr "Modify Config"
 
-#: src/routes/index.ts:147 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:158 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "Certificate Status"
@@ -1226,7 +1270,9 @@ msgstr "Single Directive"
 #: src/views/certificate/ACMEUser.vue:13
 #: src/views/certificate/CertificateEditor.vue:152
 #: src/views/certificate/CertificateList/certColumns.tsx:10
-#: src/views/certificate/DNSCredential.vue:11 src/views/config/config.ts:7
+#: src/views/certificate/DNSCredential.vue:11
+#: src/views/config/components/Mkdir.vue:67 src/views/config/configColumns.ts:8
+#: src/views/config/ConfigEditor.vue:239
 #: src/views/domain/components/RightSettings.vue:83
 #: src/views/domain/components/SiteDuplicate.vue:129
 #: src/views/domain/DomainList.vue:13
@@ -1254,6 +1300,16 @@ msgstr "Network Total Receive"
 msgid "Network Total Send"
 msgstr "Network Total Send"
 
+#: src/views/config/components/Rename.vue:70
+#, fuzzy
+msgid "New name"
+msgstr "Username"
+
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "New Path"
+msgstr "Path"
+
 #: src/views/system/Upgrade.vue:210
 msgid "New version released"
 msgstr ""
@@ -1285,7 +1341,7 @@ msgstr ""
 msgid "Nginx Error Log Path"
 msgstr ""
 
-#: src/routes/index.ts:185 src/views/nginx_log/NginxLog.vue:143
+#: src/routes/index.ts:196 src/views/nginx_log/NginxLog.vue:143
 msgid "Nginx Log"
 msgstr ""
 
@@ -1322,7 +1378,7 @@ msgstr ""
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:300
+#: src/routes/index.ts:311
 msgid "Not Found"
 msgstr "Not Found"
 
@@ -1340,7 +1396,7 @@ msgstr ""
 msgid "Notification"
 msgstr "Certificate is valid"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:231
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:242
 #, fuzzy
 msgid "Notifications"
 msgstr "Certificate is valid"
@@ -1410,6 +1466,10 @@ msgstr ""
 msgid "OpenAI"
 msgstr ""
 
+#: src/views/config/components/Rename.vue:66
+msgid "Original name"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:177
 #, fuzzy
 msgid "OS"
@@ -1441,7 +1501,7 @@ msgstr "Password"
 msgid "Password (*)"
 msgstr "Password (*)"
 
-#: src/views/config/ConfigEdit.vue:118
+#: src/views/config/ConfigEditor.vue:245
 #: src/views/domain/ngx_conf/LocationEditor.vue:118
 #: src/views/domain/ngx_conf/LocationEditor.vue:90
 msgid "Path"
@@ -1479,6 +1539,17 @@ msgid ""
 "select one of the credentialsbelow to request the API of the DNS provider."
 msgstr ""
 
+#: src/views/config/components/Rename.vue:61
+#: src/views/config/ConfigEditor.vue:232
+#, fuzzy
+msgid "Please input a filename"
+msgstr "Please input your username!"
+
+#: src/views/config/components/Mkdir.vue:59
+#, fuzzy
+msgid "Please input a folder name"
+msgstr "Please input your username!"
+
 #: src/views/domain/components/SiteDuplicate.vue:38
 #: src/views/stream/components/StreamDuplicate.vue:38
 msgid ""
@@ -1514,7 +1585,7 @@ msgstr ""
 msgid "Pre-release"
 msgstr ""
 
-#: src/routes/index.ts:249 src/views/preference/Preference.vue:105
+#: src/routes/index.ts:260 src/views/preference/Preference.vue:105
 msgid "Preference"
 msgstr ""
 
@@ -1637,11 +1708,18 @@ msgstr "Saved successfully"
 msgid "Removed successfully"
 msgstr "Saved successfully"
 
+#: src/views/config/components/Rename.vue:52
+#: src/views/config/ConfigList.vue:159
 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
 #, fuzzy
 msgid "Rename"
 msgstr "Username"
 
+#: src/views/config/components/Rename.vue:37
+#, fuzzy
+msgid "Rename successfully"
+msgstr "Enabled successfully"
+
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1695,7 +1773,7 @@ msgstr ""
 
 #: src/components/ChatGPT/ChatGPT.vue:251
 #: src/views/certificate/CertificateEditor.vue:254
-#: src/views/config/ConfigEdit.vue:96 src/views/domain/DomainEdit.vue:260
+#: src/views/config/ConfigEditor.vue:205 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/preference/Preference.vue:145 src/views/stream/StreamEdit.vue:252
 msgid "Save"
@@ -1705,7 +1783,7 @@ msgstr "Save"
 msgid "Save Directive"
 msgstr "Save Directive"
 
-#: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:46
+#: src/views/config/ConfigEditor.vue:154 src/views/domain/DomainAdd.vue:46
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
 msgid "Save error %{msg}"
 msgstr "Save error %{msg}"
@@ -1718,7 +1796,7 @@ msgstr "Save error %{msg}"
 msgid "Save successfully"
 msgstr "Saved successfully"
 
-#: src/views/config/ConfigEdit.vue:55 src/views/domain/DomainAdd.vue:37
+#: src/views/config/ConfigEditor.vue:150 src/views/domain/DomainAdd.vue:37
 #: src/views/domain/DomainEdit.vue:143
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
 #: src/views/stream/StreamEdit.vue:138
@@ -1749,7 +1827,9 @@ msgstr "Send"
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:42
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:213
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:253
-#: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81
+#: src/views/config/components/Mkdir.vue:38
+#: src/views/config/components/Rename.vue:40
+#: src/views/config/ConfigEditor.vue:93 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
@@ -1803,7 +1883,7 @@ msgstr ""
 msgid "Single Directive"
 msgstr "Single Directive"
 
-#: src/routes/index.ts:207
+#: src/routes/index.ts:218
 #, fuzzy
 msgid "Site Logs"
 msgstr "Sites List"
@@ -1913,7 +1993,7 @@ msgstr "Certificate is valid"
 msgid "Sync to"
 msgstr ""
 
-#: src/routes/index.ts:258
+#: src/routes/index.ts:269
 msgid "System"
 msgstr ""
 
@@ -1926,7 +2006,7 @@ msgstr ""
 msgid "Target"
 msgstr ""
 
-#: src/routes/index.ts:177 src/views/pty/Terminal.vue:114
+#: src/routes/index.ts:188 src/views/pty/Terminal.vue:114
 msgid "Terminal"
 msgstr "Terminal"
 
@@ -2077,18 +2157,19 @@ msgstr ""
 msgid "Trash"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:66
+#: src/components/OTP/useOTPModal.ts:70
 msgid "Two-factor authentication required"
 msgstr ""
 
 #: src/views/certificate/CertificateList/certColumns.tsx:25
-#: src/views/config/config.ts:12 src/views/notification/Notification.vue:13
+#: src/views/config/configColumns.ts:16
+#: src/views/notification/Notification.vue:13
 msgid "Type"
 msgstr ""
 
 #: src/views/certificate/ACMEUser.vue:53
-#: src/views/certificate/DNSCredential.vue:27 src/views/config/config.ts:27
-#: src/views/config/ConfigEdit.vue:121
+#: src/views/certificate/DNSCredential.vue:27
+#: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:258
 #: src/views/domain/components/RightSettings.vue:86
 #: src/views/domain/DomainList.vue:41 src/views/environment/envColumns.tsx:124
 #: src/views/stream/components/RightSettings.vue:85
@@ -2101,7 +2182,7 @@ msgstr "Updated at"
 msgid "Updated successfully"
 msgstr "Saved successfully"
 
-#: src/routes/index.ts:273 src/views/environment/Environment.vue:50
+#: src/routes/index.ts:284 src/views/environment/Environment.vue:50
 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228
 msgid "Upgrade"
 msgstr ""
@@ -2231,6 +2312,10 @@ msgstr ""
 msgid "You can check Nginx UI upgrade at this page."
 msgstr ""
 
+#, fuzzy
+#~ msgid "Rename "
+#~ msgstr "Username"
+
 #~ msgid "Certificate has expired"
 #~ msgstr "Certificate has expired"
 

+ 132 - 47
app/src/language/es/app.po

@@ -22,15 +22,15 @@ msgstr ""
 msgid "2FA Settings"
 msgstr ""
 
-#: src/routes/index.ts:266
+#: src/routes/index.ts:277
 msgid "About"
 msgstr "Acerca de"
 
-#: src/routes/index.ts:193 src/views/domain/ngx_conf/LogEntry.vue:76
+#: src/routes/index.ts:204 src/views/domain/ngx_conf/LogEntry.vue:76
 msgid "Access Logs"
 msgstr "Registros de acceso"
 
-#: src/routes/index.ts:131 src/views/certificate/ACMEUser.vue:76
+#: src/routes/index.ts:142 src/views/certificate/ACMEUser.vue:76
 #: src/views/certificate/ACMEUserSelector.vue:85
 #, fuzzy
 msgid "ACME User"
@@ -38,8 +38,9 @@ msgstr "Usuario"
 
 #: src/views/certificate/ACMEUser.vue:59
 #: src/views/certificate/CertificateList/certColumns.tsx:89
-#: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34
-#: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131
+#: src/views/certificate/DNSCredential.vue:33
+#: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
+#: src/views/environment/envColumns.tsx:131
 #: src/views/notification/Notification.vue:37
 #: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
@@ -56,6 +57,12 @@ msgstr "Acción"
 msgid "Add"
 msgstr "Agregar"
 
+#: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:128
+#: src/views/config/ConfigEditor.vue:187
+#, fuzzy
+msgid "Add Configuration"
+msgstr "Editar Configuración"
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:95
 msgid "Add Directive Below"
 msgstr "Añadir directiva a continuación"
@@ -187,9 +194,9 @@ msgid "Auto-renewal enabled for %{name}"
 msgstr "Renovación automática habilitada por %{name}"
 
 #: src/views/certificate/CertificateEditor.vue:247
-#: src/views/config/Config.vue:71 src/views/config/ConfigEdit.vue:87
-#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168
-#: src/views/stream/StreamEdit.vue:245
+#: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
+#: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
+#: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
 msgid "Back"
 msgstr "Volver"
 
@@ -217,7 +224,7 @@ msgstr ""
 msgid "Base information"
 msgstr "Información general"
 
-#: src/views/config/ConfigEdit.vue:115
+#: src/views/config/ConfigEditor.vue:224
 #: src/views/domain/components/RightSettings.vue:75
 #: src/views/preference/Preference.vue:110
 #: src/views/stream/components/RightSettings.vue:74
@@ -278,12 +285,12 @@ msgid_plural "Certificates Status"
 msgstr[0] "Estado del Certificado"
 msgstr[1] "Estado del Certificado"
 
-#: src/routes/index.ts:122
+#: src/routes/index.ts:133
 #: src/views/certificate/CertificateList/Certificate.vue:13
 msgid "Certificates"
 msgstr "Certificados"
 
-#: src/routes/index.ts:139
+#: src/routes/index.ts:150
 msgid "Certificates List"
 msgstr "Lista de Certificados"
 
@@ -303,6 +310,11 @@ msgid_plural "Changed Certificates"
 msgstr[0] "Cambiar Certificado"
 msgstr[1] "Cambiar Certificado"
 
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "Changed Path"
+msgstr "Cambiar Certificado"
+
 #: src/views/environment/BatchUpgrader.vue:161 src/views/system/Upgrade.vue:190
 msgid "Channel"
 msgstr "Canal"
@@ -350,7 +362,7 @@ msgstr "El archivo de configuración se probó exitosamente"
 msgid "Configuration Name"
 msgstr "Nombre de la configuración"
 
-#: src/views/config/Config.vue:42
+#: src/views/config/ConfigList.vue:91
 msgid "Configurations"
 msgstr "Configuraciones"
 
@@ -400,10 +412,25 @@ msgstr "Crear"
 msgid "Create Another"
 msgstr "Crear otro"
 
+#: src/views/config/ConfigList.vue:109
+#, fuzzy
+msgid "Create File"
+msgstr "Crear"
+
+#: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
+#, fuzzy
+msgid "Create Folder"
+msgstr "Crear otro"
+
 #: src/views/notification/Notification.vue:31 src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgstr "Creado el"
 
+#: src/views/config/components/Mkdir.vue:35
+#, fuzzy
+msgid "Created successfully"
+msgstr "Limpiado exitoso"
+
 #: src/language/constants.ts:9
 msgid "Creating client facilitates communication with the CA server"
 msgstr "La creación de un cliente facilita la comunicación con el servidor CA"
@@ -439,7 +466,8 @@ msgid ""
 "indicator."
 msgstr ""
 
-#: src/routes/index.ts:39
+#: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
+#: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
 msgid "Dashboard"
 msgstr "Panel"
 
@@ -518,7 +546,7 @@ msgstr "Directiva"
 msgid "Directives"
 msgstr "Directivas"
 
-#: src/views/config/config.ts:18
+#: src/views/config/configColumns.ts:22
 msgid "Directory"
 msgstr "Directorio"
 
@@ -548,7 +576,7 @@ msgstr "Desactivado con éxito"
 msgid "Disk IO"
 msgstr "I/O del disco"
 
-#: src/routes/index.ts:167 src/views/certificate/DNSCredential.vue:40
+#: src/routes/index.ts:178 src/views/certificate/DNSCredential.vue:40
 msgid "DNS Credentials"
 msgstr "Credenciales de DNS"
 
@@ -661,7 +689,7 @@ msgstr "Duplicado con éxito a local"
 msgid "Edit %{n}"
 msgstr "Editar %{n}"
 
-#: src/routes/index.ts:112 src/views/config/ConfigEdit.vue:81
+#: src/routes/index.ts:122 src/views/config/ConfigEditor.vue:187
 msgid "Edit Configuration"
 msgstr "Editar Configuración"
 
@@ -750,7 +778,11 @@ msgstr "Habilitado con éxito"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Encriptar sitio web con Let's Encrypt"
 
-#: src/routes/index.ts:217 src/views/environment/Environment.vue:34
+#: src/views/config/ConfigList.vue:151
+msgid "Enter"
+msgstr ""
+
+#: src/routes/index.ts:228 src/views/environment/Environment.vue:34
 msgid "Environment"
 msgstr "Entorno"
 
@@ -767,7 +799,7 @@ msgstr "Entornos"
 msgid "Error"
 msgstr "Error"
 
-#: src/routes/index.ts:200 src/views/domain/ngx_conf/LogEntry.vue:84
+#: src/routes/index.ts:211 src/views/domain/ngx_conf/LogEntry.vue:84
 msgid "Error Logs"
 msgstr "Registros de acceso"
 
@@ -816,7 +848,7 @@ msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr ""
 "No se pudo guardar, se detectó un error(es) de sintaxis en la configuración."
 
-#: src/views/config/config.ts:20
+#: src/views/config/configColumns.ts:24
 msgid "File"
 msgstr "Archivo"
 
@@ -845,15 +877,15 @@ msgstr "Terminado"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr "Para usuario chino: https://mirror.ghproxy.com/"
 
-#: src/views/config/ConfigEdit.vue:90
+#: src/views/config/ConfigEditor.vue:199
 msgid "Format Code"
 msgstr "Código de formato"
 
-#: src/views/config/ConfigEdit.vue:68
+#: src/views/config/ConfigEditor.vue:166
 msgid "Format error %{msg}"
 msgstr "Error de formato %{msg}"
 
-#: src/views/config/ConfigEdit.vue:66
+#: src/views/config/ConfigEditor.vue:164
 msgid "Format successfully"
 msgstr "Formateado correctamente"
 
@@ -931,7 +963,7 @@ msgstr ""
 msgid "Import"
 msgstr "Importar"
 
-#: src/routes/index.ts:157 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:168 src/views/certificate/CertificateEditor.vue:79
 msgid "Import Certificate"
 msgstr "Importar Certificado"
 
@@ -961,7 +993,7 @@ msgstr ""
 msgid "Input the recovery code:"
 msgstr ""
 
-#: src/routes/index.ts:288 src/views/other/Install.vue:134
+#: src/routes/index.ts:299 src/views/other/Install.vue:134
 msgid "Install"
 msgstr "Instalar"
 
@@ -982,7 +1014,17 @@ msgstr "Válido"
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:60
+#: src/views/config/components/Rename.vue:62
+#: src/views/config/ConfigEditor.vue:233
+#, fuzzy
+msgid "Invalid filename"
+msgstr "Válido"
+
+#: src/views/config/components/Mkdir.vue:60
+msgid "Invalid folder name"
+msgstr ""
+
+#: src/components/OTP/useOTPModal.ts:64
 msgid "Invalid passcode or recovery code"
 msgstr ""
 
@@ -1080,7 +1122,7 @@ msgstr "Ubicaciones"
 msgid "Log"
 msgstr "Registro"
 
-#: src/routes/index.ts:294 src/views/other/Login.vue:192
+#: src/routes/index.ts:305 src/views/other/Login.vue:192
 msgid "Login"
 msgstr "Acceso"
 
@@ -1114,7 +1156,8 @@ msgstr ""
 "Asegúrese de haber configurado un proxy reverso para el directorio .well-"
 "known en HTTPChallengePort antes de obtener el certificado."
 
-#: src/routes/index.ts:102
+#: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
+#: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
 msgid "Manage Configs"
 msgstr "Administrar configuraciones"
 
@@ -1126,7 +1169,7 @@ msgstr "Administrar sitios"
 msgid "Manage Streams"
 msgstr "Administrar Transmisiones"
 
-#: src/routes/index.ts:240 src/views/user/User.vue:9
+#: src/routes/index.ts:251 src/views/user/User.vue:9
 msgid "Manage Users"
 msgstr "Administrar usuarios"
 
@@ -1159,10 +1202,11 @@ msgstr "Modo de ejecución"
 #: src/components/ChatGPT/ChatGPT.vue:248
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
+#: src/views/config/ConfigList.vue:151
 msgid "Modify"
 msgstr "Modificar"
 
-#: src/routes/index.ts:147 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:158 src/views/certificate/CertificateEditor.vue:79
 msgid "Modify Certificate"
 msgstr "Modificar Certificado"
 
@@ -1182,7 +1226,9 @@ msgstr "Directiva multilínea"
 #: src/views/certificate/ACMEUser.vue:13
 #: src/views/certificate/CertificateEditor.vue:152
 #: src/views/certificate/CertificateList/certColumns.tsx:10
-#: src/views/certificate/DNSCredential.vue:11 src/views/config/config.ts:7
+#: src/views/certificate/DNSCredential.vue:11
+#: src/views/config/components/Mkdir.vue:67 src/views/config/configColumns.ts:8
+#: src/views/config/ConfigEditor.vue:239
 #: src/views/domain/components/RightSettings.vue:83
 #: src/views/domain/components/SiteDuplicate.vue:129
 #: src/views/domain/DomainList.vue:13
@@ -1210,6 +1256,16 @@ msgstr "Total recibido por la red"
 msgid "Network Total Send"
 msgstr "Total enviado por la red"
 
+#: src/views/config/components/Rename.vue:70
+#, fuzzy
+msgid "New name"
+msgstr "Renombrar"
+
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "New Path"
+msgstr "Ruta"
+
 #: src/views/system/Upgrade.vue:210
 msgid "New version released"
 msgstr "Se liberó una nueva versión"
@@ -1240,7 +1296,7 @@ msgstr "Control de Nginx"
 msgid "Nginx Error Log Path"
 msgstr "Ruta de registro de errores de Nginx"
 
-#: src/routes/index.ts:185 src/views/nginx_log/NginxLog.vue:143
+#: src/routes/index.ts:196 src/views/nginx_log/NginxLog.vue:143
 msgid "Nginx Log"
 msgstr "Registro Nginx"
 
@@ -1275,7 +1331,7 @@ msgstr "Secreto del nodo"
 msgid "Not After"
 msgstr "No después de"
 
-#: src/routes/index.ts:300
+#: src/routes/index.ts:311
 msgid "Not Found"
 msgstr "No encontrado"
 
@@ -1292,7 +1348,7 @@ msgstr "Nota"
 msgid "Notification"
 msgstr "Notificación"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:231
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:242
 msgid "Notifications"
 msgstr "Notificaciones"
 
@@ -1360,6 +1416,10 @@ msgstr "En línea"
 msgid "OpenAI"
 msgstr "OpenAI"
 
+#: src/views/config/components/Rename.vue:66
+msgid "Original name"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:177
 msgid "OS"
 msgstr "SO"
@@ -1390,7 +1450,7 @@ msgstr "Contraseña"
 msgid "Password (*)"
 msgstr "Contraseña (*)"
 
-#: src/views/config/ConfigEdit.vue:118
+#: src/views/config/ConfigEditor.vue:245
 #: src/views/domain/ngx_conf/LocationEditor.vue:118
 #: src/views/domain/ngx_conf/LocationEditor.vue:90
 msgid "Path"
@@ -1433,6 +1493,17 @@ msgstr ""
 "luego seleccione una de las credenciales de aquí debajo para llamar a la API "
 "del proveedor de DNS."
 
+#: src/views/config/components/Rename.vue:61
+#: src/views/config/ConfigEditor.vue:232
+#, fuzzy
+msgid "Please input a filename"
+msgstr "¡Por favor ingrese su nombre de usuario!"
+
+#: src/views/config/components/Mkdir.vue:59
+#, fuzzy
+msgid "Please input a folder name"
+msgstr "¡Por favor ingrese su nombre de usuario!"
+
 #: src/views/domain/components/SiteDuplicate.vue:38
 #: src/views/stream/components/StreamDuplicate.vue:38
 msgid ""
@@ -1472,7 +1543,7 @@ msgstr "¡Seleccione al menos un nodo!"
 msgid "Pre-release"
 msgstr "Prelanzamiento"
 
-#: src/routes/index.ts:249 src/views/preference/Preference.vue:105
+#: src/routes/index.ts:260 src/views/preference/Preference.vue:105
 msgid "Preference"
 msgstr "Configuración"
 
@@ -1594,10 +1665,17 @@ msgstr "Eliminado con éxito"
 msgid "Removed successfully"
 msgstr "Eliminado con éxito"
 
+#: src/views/config/components/Rename.vue:52
+#: src/views/config/ConfigList.vue:159
 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
 msgid "Rename"
 msgstr "Renombrar"
 
+#: src/views/config/components/Rename.vue:37
+#, fuzzy
+msgid "Rename successfully"
+msgstr "Renovado con éxito"
+
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 msgid "Renew Certificate"
@@ -1647,7 +1725,7 @@ msgstr "Corriendo"
 
 #: src/components/ChatGPT/ChatGPT.vue:251
 #: src/views/certificate/CertificateEditor.vue:254
-#: src/views/config/ConfigEdit.vue:96 src/views/domain/DomainEdit.vue:260
+#: src/views/config/ConfigEditor.vue:205 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/preference/Preference.vue:145 src/views/stream/StreamEdit.vue:252
 msgid "Save"
@@ -1657,7 +1735,7 @@ msgstr "Guardar"
 msgid "Save Directive"
 msgstr "Guardar Directiva"
 
-#: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:46
+#: src/views/config/ConfigEditor.vue:154 src/views/domain/DomainAdd.vue:46
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
 msgid "Save error %{msg}"
 msgstr "Error al guardar %{msg}"
@@ -1669,7 +1747,7 @@ msgstr "Error al guardar %{msg}"
 msgid "Save successfully"
 msgstr "Guardado con éxito"
 
-#: src/views/config/ConfigEdit.vue:55 src/views/domain/DomainAdd.vue:37
+#: src/views/config/ConfigEditor.vue:150 src/views/domain/DomainAdd.vue:37
 #: src/views/domain/DomainEdit.vue:143
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
 #: src/views/stream/StreamEdit.vue:138
@@ -1700,7 +1778,9 @@ msgstr "Enviado"
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:42
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:213
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:253
-#: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81
+#: src/views/config/components/Mkdir.vue:38
+#: src/views/config/components/Rename.vue:40
+#: src/views/config/ConfigEditor.vue:93 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
@@ -1754,7 +1834,7 @@ msgstr ""
 msgid "Single Directive"
 msgstr "Directiva de una sola línea"
 
-#: src/routes/index.ts:207
+#: src/routes/index.ts:218
 msgid "Site Logs"
 msgstr "Registros del sitio"
 
@@ -1857,7 +1937,7 @@ msgstr "Renovado de Certificado exitoso"
 msgid "Sync to"
 msgstr ""
 
-#: src/routes/index.ts:258
+#: src/routes/index.ts:269
 msgid "System"
 msgstr "Sistema"
 
@@ -1870,7 +1950,7 @@ msgstr ""
 msgid "Target"
 msgstr "Objetivo"
 
-#: src/routes/index.ts:177 src/views/pty/Terminal.vue:114
+#: src/routes/index.ts:188 src/views/pty/Terminal.vue:114
 msgid "Terminal"
 msgstr "Terminal"
 
@@ -2026,18 +2106,19 @@ msgstr ""
 msgid "Trash"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:66
+#: src/components/OTP/useOTPModal.ts:70
 msgid "Two-factor authentication required"
 msgstr ""
 
 #: src/views/certificate/CertificateList/certColumns.tsx:25
-#: src/views/config/config.ts:12 src/views/notification/Notification.vue:13
+#: src/views/config/configColumns.ts:16
+#: src/views/notification/Notification.vue:13
 msgid "Type"
 msgstr "Tipo"
 
 #: src/views/certificate/ACMEUser.vue:53
-#: src/views/certificate/DNSCredential.vue:27 src/views/config/config.ts:27
-#: src/views/config/ConfigEdit.vue:121
+#: src/views/certificate/DNSCredential.vue:27
+#: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:258
 #: src/views/domain/components/RightSettings.vue:86
 #: src/views/domain/DomainList.vue:41 src/views/environment/envColumns.tsx:124
 #: src/views/stream/components/RightSettings.vue:85
@@ -2049,7 +2130,7 @@ msgstr "Actualizado a"
 msgid "Updated successfully"
 msgstr "Actualización exitosa"
 
-#: src/routes/index.ts:273 src/views/environment/Environment.vue:50
+#: src/routes/index.ts:284 src/views/environment/Environment.vue:50
 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228
 msgid "Upgrade"
 msgstr "Actualizar"
@@ -2182,6 +2263,10 @@ msgstr "Estás usando la última versión"
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "Puede consultar la actualización de Nginx UI en esta página."
 
+#, fuzzy
+#~ msgid "Rename "
+#~ msgstr "Renombrar"
+
 #~ msgid "Auto Cert"
 #~ msgstr "Certificado automático"
 

+ 131 - 47
app/src/language/fr_FR/app.po

@@ -19,15 +19,15 @@ msgstr ""
 msgid "2FA Settings"
 msgstr ""
 
-#: src/routes/index.ts:266
+#: src/routes/index.ts:277
 msgid "About"
 msgstr "À propos"
 
-#: src/routes/index.ts:193 src/views/domain/ngx_conf/LogEntry.vue:76
+#: src/routes/index.ts:204 src/views/domain/ngx_conf/LogEntry.vue:76
 msgid "Access Logs"
 msgstr "Journaux d'accès"
 
-#: src/routes/index.ts:131 src/views/certificate/ACMEUser.vue:76
+#: src/routes/index.ts:142 src/views/certificate/ACMEUser.vue:76
 #: src/views/certificate/ACMEUserSelector.vue:85
 #, fuzzy
 msgid "ACME User"
@@ -35,8 +35,9 @@ msgstr "Nom d'utilisateur"
 
 #: src/views/certificate/ACMEUser.vue:59
 #: src/views/certificate/CertificateList/certColumns.tsx:89
-#: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34
-#: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131
+#: src/views/certificate/DNSCredential.vue:33
+#: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
+#: src/views/environment/envColumns.tsx:131
 #: src/views/notification/Notification.vue:37
 #: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
@@ -53,6 +54,12 @@ msgstr "Action"
 msgid "Add"
 msgstr "Ajouter"
 
+#: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:128
+#: src/views/config/ConfigEditor.vue:187
+#, fuzzy
+msgid "Add Configuration"
+msgstr "Modifier la configuration"
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:95
 msgid "Add Directive Below"
 msgstr "Ajouter une directive"
@@ -190,9 +197,9 @@ msgid "Auto-renewal enabled for %{name}"
 msgstr "Renouvellement automatique activé pour %{name}"
 
 #: src/views/certificate/CertificateEditor.vue:247
-#: src/views/config/Config.vue:71 src/views/config/ConfigEdit.vue:87
-#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168
-#: src/views/stream/StreamEdit.vue:245
+#: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
+#: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
+#: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
 msgid "Back"
 msgstr "Retour"
 
@@ -220,7 +227,7 @@ msgstr ""
 msgid "Base information"
 msgstr "Information générale"
 
-#: src/views/config/ConfigEdit.vue:115
+#: src/views/config/ConfigEditor.vue:224
 #: src/views/domain/components/RightSettings.vue:75
 #: src/views/preference/Preference.vue:110
 #: src/views/stream/components/RightSettings.vue:74
@@ -282,13 +289,13 @@ msgid_plural "Certificates Status"
 msgstr[0] "État du certificat"
 msgstr[1] "État du certificat"
 
-#: src/routes/index.ts:122
+#: src/routes/index.ts:133
 #: src/views/certificate/CertificateList/Certificate.vue:13
 #, fuzzy
 msgid "Certificates"
 msgstr "État du certificat"
 
-#: src/routes/index.ts:139
+#: src/routes/index.ts:150
 #, fuzzy
 msgid "Certificates List"
 msgstr "Liste des certifications"
@@ -309,6 +316,11 @@ msgid_plural "Changed Certificates"
 msgstr[0] "Changer de certificat"
 msgstr[1] "Changer de certificat"
 
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "Changed Path"
+msgstr "Changer de certificat"
+
 #: src/views/environment/BatchUpgrader.vue:161 src/views/system/Upgrade.vue:190
 msgid "Channel"
 msgstr ""
@@ -357,7 +369,7 @@ msgstr "Le fichier de configuration est testé avec succès"
 msgid "Configuration Name"
 msgstr "Nom de la configuration"
 
-#: src/views/config/Config.vue:42
+#: src/views/config/ConfigList.vue:91
 msgid "Configurations"
 msgstr "Configurations"
 
@@ -408,10 +420,25 @@ msgstr "Créé le"
 msgid "Create Another"
 msgstr "Créer un autre"
 
+#: src/views/config/ConfigList.vue:109
+#, fuzzy
+msgid "Create File"
+msgstr "Créé le"
+
+#: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
+#, fuzzy
+msgid "Create Folder"
+msgstr "Créer un autre"
+
 #: src/views/notification/Notification.vue:31 src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgstr "Créé le"
 
+#: src/views/config/components/Mkdir.vue:35
+#, fuzzy
+msgid "Created successfully"
+msgstr "Désactivé avec succès"
+
 #: src/language/constants.ts:9
 msgid "Creating client facilitates communication with the CA server"
 msgstr "La création du client facilite la communication avec le serveur CA"
@@ -447,7 +474,8 @@ msgid ""
 "indicator."
 msgstr ""
 
-#: src/routes/index.ts:39
+#: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
+#: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
 msgid "Dashboard"
 msgstr "Dashboard"
 
@@ -529,7 +557,7 @@ msgstr "Directive"
 msgid "Directives"
 msgstr "Directives"
 
-#: src/views/config/config.ts:18
+#: src/views/config/configColumns.ts:22
 #, fuzzy
 msgid "Directory"
 msgstr "Directive"
@@ -561,7 +589,7 @@ msgstr "Désactivé avec succès"
 msgid "Disk IO"
 msgstr "E/S disque"
 
-#: src/routes/index.ts:167 src/views/certificate/DNSCredential.vue:40
+#: src/routes/index.ts:178 src/views/certificate/DNSCredential.vue:40
 msgid "DNS Credentials"
 msgstr "Identifiants DNS"
 
@@ -683,7 +711,7 @@ msgstr "Dupliqué avec succès"
 msgid "Edit %{n}"
 msgstr "Modifier %{n}"
 
-#: src/routes/index.ts:112 src/views/config/ConfigEdit.vue:81
+#: src/routes/index.ts:122 src/views/config/ConfigEditor.vue:187
 msgid "Edit Configuration"
 msgstr "Modifier la configuration"
 
@@ -775,7 +803,11 @@ msgstr "Activé avec succès"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Crypter le site Web avec Let's Encrypt"
 
-#: src/routes/index.ts:217 src/views/environment/Environment.vue:34
+#: src/views/config/ConfigList.vue:151
+msgid "Enter"
+msgstr ""
+
+#: src/routes/index.ts:228 src/views/environment/Environment.vue:34
 msgid "Environment"
 msgstr ""
 
@@ -793,7 +825,7 @@ msgstr "Commentaires"
 msgid "Error"
 msgstr "Erreur"
 
-#: src/routes/index.ts:200 src/views/domain/ngx_conf/LogEntry.vue:84
+#: src/routes/index.ts:211 src/views/domain/ngx_conf/LogEntry.vue:84
 msgid "Error Logs"
 msgstr "Journaux d'erreurs"
 
@@ -844,7 +876,7 @@ msgstr ""
 "Échec de l'enregistrement, une ou plusieurs erreurs de syntaxe ont été "
 "détectées dans la configuration."
 
-#: src/views/config/config.ts:20
+#: src/views/config/configColumns.ts:24
 msgid "File"
 msgstr "Fichier"
 
@@ -875,15 +907,15 @@ msgstr "Finie"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr "Utilisateur chinois : https://mirror.ghproxy.com/"
 
-#: src/views/config/ConfigEdit.vue:90
+#: src/views/config/ConfigEditor.vue:199
 msgid "Format Code"
 msgstr "Code de formatage"
 
-#: src/views/config/ConfigEdit.vue:68
+#: src/views/config/ConfigEditor.vue:166
 msgid "Format error %{msg}"
 msgstr "Erreur de format %{msg}"
 
-#: src/views/config/ConfigEdit.vue:66
+#: src/views/config/ConfigEditor.vue:164
 msgid "Format successfully"
 msgstr "Formaté avec succès"
 
@@ -963,7 +995,7 @@ msgstr ""
 msgid "Import"
 msgstr "Exporter"
 
-#: src/routes/index.ts:157 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:168 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Import Certificate"
 msgstr "État du certificat"
@@ -994,7 +1026,7 @@ msgstr ""
 msgid "Input the recovery code:"
 msgstr ""
 
-#: src/routes/index.ts:288 src/views/other/Install.vue:134
+#: src/routes/index.ts:299 src/views/other/Install.vue:134
 msgid "Install"
 msgstr "Installer"
 
@@ -1014,7 +1046,16 @@ msgstr ""
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:60
+#: src/views/config/components/Rename.vue:62
+#: src/views/config/ConfigEditor.vue:233
+msgid "Invalid filename"
+msgstr ""
+
+#: src/views/config/components/Mkdir.vue:60
+msgid "Invalid folder name"
+msgstr ""
+
+#: src/components/OTP/useOTPModal.ts:64
 msgid "Invalid passcode or recovery code"
 msgstr ""
 
@@ -1121,7 +1162,7 @@ msgstr "Localisations"
 msgid "Log"
 msgstr "Connexion"
 
-#: src/routes/index.ts:294 src/views/other/Login.vue:192
+#: src/routes/index.ts:305 src/views/other/Login.vue:192
 msgid "Login"
 msgstr "Connexion"
 
@@ -1156,7 +1197,8 @@ msgstr ""
 "Assurez vous d'avoir configuré un reverse proxy pour le répertoire .well-"
 "known vers HTTPChallengePort avant d'obtenir le certificat."
 
-#: src/routes/index.ts:102
+#: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
+#: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
 msgid "Manage Configs"
 msgstr "Gérer les configurations"
 
@@ -1169,7 +1211,7 @@ msgstr "Gérer les sites"
 msgid "Manage Streams"
 msgstr "Gérer les sites"
 
-#: src/routes/index.ts:240 src/views/user/User.vue:9
+#: src/routes/index.ts:251 src/views/user/User.vue:9
 msgid "Manage Users"
 msgstr "Gérer les utilisateurs"
 
@@ -1203,10 +1245,11 @@ msgstr "Mode d'exécution"
 #: src/components/ChatGPT/ChatGPT.vue:248
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
+#: src/views/config/ConfigList.vue:151
 msgid "Modify"
 msgstr "Modifier"
 
-#: src/routes/index.ts:147 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:158 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "État du certificat"
@@ -1227,7 +1270,9 @@ msgstr "Directive multiligne"
 #: src/views/certificate/ACMEUser.vue:13
 #: src/views/certificate/CertificateEditor.vue:152
 #: src/views/certificate/CertificateList/certColumns.tsx:10
-#: src/views/certificate/DNSCredential.vue:11 src/views/config/config.ts:7
+#: src/views/certificate/DNSCredential.vue:11
+#: src/views/config/components/Mkdir.vue:67 src/views/config/configColumns.ts:8
+#: src/views/config/ConfigEditor.vue:239
 #: src/views/domain/components/RightSettings.vue:83
 #: src/views/domain/components/SiteDuplicate.vue:129
 #: src/views/domain/DomainList.vue:13
@@ -1255,6 +1300,16 @@ msgstr "Réception totale du réseau"
 msgid "Network Total Send"
 msgstr "Envoi total réseau"
 
+#: src/views/config/components/Rename.vue:70
+#, fuzzy
+msgid "New name"
+msgstr "Nom d'utilisateur"
+
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "New Path"
+msgstr "Chemin"
+
 #: src/views/system/Upgrade.vue:210
 msgid "New version released"
 msgstr "Nouvelle version publiée"
@@ -1286,7 +1341,7 @@ msgstr "Contrôle Nginx"
 msgid "Nginx Error Log Path"
 msgstr "Chemin du journal des erreurs Nginx"
 
-#: src/routes/index.ts:185 src/views/nginx_log/NginxLog.vue:143
+#: src/routes/index.ts:196 src/views/nginx_log/NginxLog.vue:143
 msgid "Nginx Log"
 msgstr "Journal Nginx"
 
@@ -1322,7 +1377,7 @@ msgstr "Secret Jwt"
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:300
+#: src/routes/index.ts:311
 msgid "Not Found"
 msgstr "Introuvable"
 
@@ -1340,7 +1395,7 @@ msgstr "Note"
 msgid "Notification"
 msgstr "Certification"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:231
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:242
 #, fuzzy
 msgid "Notifications"
 msgstr "Certification"
@@ -1409,6 +1464,10 @@ msgstr ""
 msgid "OpenAI"
 msgstr "OpenAI"
 
+#: src/views/config/components/Rename.vue:66
+msgid "Original name"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:177
 msgid "OS"
 msgstr "OS"
@@ -1439,7 +1498,7 @@ msgstr "Mot de passe"
 msgid "Password (*)"
 msgstr "Mot de passe (*)"
 
-#: src/views/config/ConfigEdit.vue:118
+#: src/views/config/ConfigEditor.vue:245
 #: src/views/domain/ngx_conf/LocationEditor.vue:118
 #: src/views/domain/ngx_conf/LocationEditor.vue:90
 msgid "Path"
@@ -1482,6 +1541,17 @@ msgstr ""
 "des informations d'identification ci-dessous pour demander l'API du "
 "fournisseur DNS."
 
+#: src/views/config/components/Rename.vue:61
+#: src/views/config/ConfigEditor.vue:232
+#, fuzzy
+msgid "Please input a filename"
+msgstr "Veuillez saisir votre nom d'utilisateur !"
+
+#: src/views/config/components/Mkdir.vue:59
+#, fuzzy
+msgid "Please input a folder name"
+msgstr "Veuillez saisir votre nom d'utilisateur !"
+
 #: src/views/domain/components/SiteDuplicate.vue:38
 #: src/views/stream/components/StreamDuplicate.vue:38
 msgid ""
@@ -1519,7 +1589,7 @@ msgstr ""
 msgid "Pre-release"
 msgstr ""
 
-#: src/routes/index.ts:249 src/views/preference/Preference.vue:105
+#: src/routes/index.ts:260 src/views/preference/Preference.vue:105
 msgid "Preference"
 msgstr "Préférence"
 
@@ -1645,11 +1715,18 @@ msgstr "Enregistré avec succès"
 msgid "Removed successfully"
 msgstr "Enregistré avec succès"
 
+#: src/views/config/components/Rename.vue:52
+#: src/views/config/ConfigList.vue:159
 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
 #, fuzzy
 msgid "Rename"
 msgstr "Nom d'utilisateur"
 
+#: src/views/config/components/Rename.vue:37
+#, fuzzy
+msgid "Rename successfully"
+msgstr "Activé avec succès"
+
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1703,7 +1780,7 @@ msgstr "En cours d'éxécution"
 
 #: src/components/ChatGPT/ChatGPT.vue:251
 #: src/views/certificate/CertificateEditor.vue:254
-#: src/views/config/ConfigEdit.vue:96 src/views/domain/DomainEdit.vue:260
+#: src/views/config/ConfigEditor.vue:205 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/preference/Preference.vue:145 src/views/stream/StreamEdit.vue:252
 msgid "Save"
@@ -1713,7 +1790,7 @@ msgstr "Enregistrer"
 msgid "Save Directive"
 msgstr "Enregistrer la directive"
 
-#: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:46
+#: src/views/config/ConfigEditor.vue:154 src/views/domain/DomainAdd.vue:46
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
 msgid "Save error %{msg}"
 msgstr "Enregistrer l'erreur %{msg}"
@@ -1725,7 +1802,7 @@ msgstr "Enregistrer l'erreur %{msg}"
 msgid "Save successfully"
 msgstr "Sauvegarde réussie"
 
-#: src/views/config/ConfigEdit.vue:55 src/views/domain/DomainAdd.vue:37
+#: src/views/config/ConfigEditor.vue:150 src/views/domain/DomainAdd.vue:37
 #: src/views/domain/DomainEdit.vue:143
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
 #: src/views/stream/StreamEdit.vue:138
@@ -1756,7 +1833,9 @@ msgstr "Envoyer"
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:42
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:213
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:253
-#: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81
+#: src/views/config/components/Mkdir.vue:38
+#: src/views/config/components/Rename.vue:40
+#: src/views/config/ConfigEditor.vue:93 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
@@ -1812,7 +1891,7 @@ msgstr ""
 msgid "Single Directive"
 msgstr "Directive unique"
 
-#: src/routes/index.ts:207
+#: src/routes/index.ts:218
 msgid "Site Logs"
 msgstr "Journaux du site"
 
@@ -1920,7 +1999,7 @@ msgstr "Changer de certificat"
 msgid "Sync to"
 msgstr ""
 
-#: src/routes/index.ts:258
+#: src/routes/index.ts:269
 msgid "System"
 msgstr "Système"
 
@@ -1933,7 +2012,7 @@ msgstr ""
 msgid "Target"
 msgstr ""
 
-#: src/routes/index.ts:177 src/views/pty/Terminal.vue:114
+#: src/routes/index.ts:188 src/views/pty/Terminal.vue:114
 msgid "Terminal"
 msgstr "Terminal"
 
@@ -2092,18 +2171,19 @@ msgstr ""
 msgid "Trash"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:66
+#: src/components/OTP/useOTPModal.ts:70
 msgid "Two-factor authentication required"
 msgstr ""
 
 #: src/views/certificate/CertificateList/certColumns.tsx:25
-#: src/views/config/config.ts:12 src/views/notification/Notification.vue:13
+#: src/views/config/configColumns.ts:16
+#: src/views/notification/Notification.vue:13
 msgid "Type"
 msgstr "Type"
 
 #: src/views/certificate/ACMEUser.vue:53
-#: src/views/certificate/DNSCredential.vue:27 src/views/config/config.ts:27
-#: src/views/config/ConfigEdit.vue:121
+#: src/views/certificate/DNSCredential.vue:27
+#: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:258
 #: src/views/domain/components/RightSettings.vue:86
 #: src/views/domain/DomainList.vue:41 src/views/environment/envColumns.tsx:124
 #: src/views/stream/components/RightSettings.vue:85
@@ -2115,7 +2195,7 @@ msgstr "Mis à jour le"
 msgid "Updated successfully"
 msgstr "Mis à jour avec succés"
 
-#: src/routes/index.ts:273 src/views/environment/Environment.vue:50
+#: src/routes/index.ts:284 src/views/environment/Environment.vue:50
 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228
 msgid "Upgrade"
 msgstr "Mettre à niveau"
@@ -2247,6 +2327,10 @@ msgstr "Vous utilisez la dernière version"
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "Vous pouvez vérifier la mise à niveau de Nginx UI sur cette page."
 
+#, fuzzy
+#~ msgid "Rename "
+#~ msgstr "Nom d'utilisateur"
+
 #~ msgid "Auto Cert"
 #~ msgstr "Auto Cert"
 

+ 133 - 50
app/src/language/ko_KR/app.po

@@ -21,15 +21,15 @@ msgstr ""
 msgid "2FA Settings"
 msgstr ""
 
-#: src/routes/index.ts:266
+#: src/routes/index.ts:277
 msgid "About"
 msgstr "소개"
 
-#: src/routes/index.ts:193 src/views/domain/ngx_conf/LogEntry.vue:76
+#: src/routes/index.ts:204 src/views/domain/ngx_conf/LogEntry.vue:76
 msgid "Access Logs"
 msgstr "접근 로그"
 
-#: src/routes/index.ts:131 src/views/certificate/ACMEUser.vue:76
+#: src/routes/index.ts:142 src/views/certificate/ACMEUser.vue:76
 #: src/views/certificate/ACMEUserSelector.vue:85
 #, fuzzy
 msgid "ACME User"
@@ -37,8 +37,9 @@ msgstr "사용자 이름"
 
 #: src/views/certificate/ACMEUser.vue:59
 #: src/views/certificate/CertificateList/certColumns.tsx:89
-#: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34
-#: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131
+#: src/views/certificate/DNSCredential.vue:33
+#: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
+#: src/views/environment/envColumns.tsx:131
 #: src/views/notification/Notification.vue:37
 #: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
@@ -55,6 +56,12 @@ msgstr "작업"
 msgid "Add"
 msgstr "추가"
 
+#: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:128
+#: src/views/config/ConfigEditor.vue:187
+#, fuzzy
+msgid "Add Configuration"
+msgstr "구성 편집"
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:95
 msgid "Add Directive Below"
 msgstr "아래에 지시문 추가"
@@ -186,9 +193,9 @@ msgid "Auto-renewal enabled for %{name}"
 msgstr "%{name}에 대한 자동 갱신 활성화됨"
 
 #: src/views/certificate/CertificateEditor.vue:247
-#: src/views/config/Config.vue:71 src/views/config/ConfigEdit.vue:87
-#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168
-#: src/views/stream/StreamEdit.vue:245
+#: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
+#: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
+#: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
 msgid "Back"
 msgstr "뒤로"
 
@@ -216,7 +223,7 @@ msgstr ""
 msgid "Base information"
 msgstr "기본 정보"
 
-#: src/views/config/ConfigEdit.vue:115
+#: src/views/config/ConfigEditor.vue:224
 #: src/views/domain/components/RightSettings.vue:75
 #: src/views/preference/Preference.vue:110
 #: src/views/stream/components/RightSettings.vue:74
@@ -277,12 +284,12 @@ msgid_plural "Certificates Status"
 msgstr[0] "인증서 상태"
 msgstr[1] "인증서 상태"
 
-#: src/routes/index.ts:122
+#: src/routes/index.ts:133
 #: src/views/certificate/CertificateList/Certificate.vue:13
 msgid "Certificates"
 msgstr "인증서"
 
-#: src/routes/index.ts:139
+#: src/routes/index.ts:150
 msgid "Certificates List"
 msgstr "인증서 목록"
 
@@ -302,6 +309,11 @@ msgid_plural "Changed Certificates"
 msgstr[0] "인증서 변경"
 msgstr[1] "인증서 변경"
 
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "Changed Path"
+msgstr "인증서 변경"
+
 #: src/views/environment/BatchUpgrader.vue:161 src/views/system/Upgrade.vue:190
 msgid "Channel"
 msgstr "채널"
@@ -348,7 +360,7 @@ msgstr "구성 파일 테스트 성공"
 msgid "Configuration Name"
 msgstr "구성 이름"
 
-#: src/views/config/Config.vue:42
+#: src/views/config/ConfigList.vue:91
 msgid "Configurations"
 msgstr "구성들"
 
@@ -398,10 +410,25 @@ msgstr "생성"
 msgid "Create Another"
 msgstr "다른 것 생성하기"
 
+#: src/views/config/ConfigList.vue:109
+#, fuzzy
+msgid "Create File"
+msgstr "생성"
+
+#: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
+#, fuzzy
+msgid "Create Folder"
+msgstr "다른 것 생성하기"
+
 #: src/views/notification/Notification.vue:31 src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgstr "생성 시간"
 
+#: src/views/config/components/Mkdir.vue:35
+#, fuzzy
+msgid "Created successfully"
+msgstr "성공적으로 제거됨"
+
 #: src/language/constants.ts:9
 msgid "Creating client facilitates communication with the CA server"
 msgstr "클라이언트 생성은 CA 서버와의 통신을 용이하게 합니다"
@@ -437,7 +464,8 @@ msgid ""
 "indicator."
 msgstr ""
 
-#: src/routes/index.ts:39
+#: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
+#: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
 msgid "Dashboard"
 msgstr "대시보드"
 
@@ -516,7 +544,7 @@ msgstr "지시문"
 msgid "Directives"
 msgstr "지시문들"
 
-#: src/views/config/config.ts:18
+#: src/views/config/configColumns.ts:22
 msgid "Directory"
 msgstr "디렉토리"
 
@@ -546,7 +574,7 @@ msgstr "성공적으로 비활성화됨"
 msgid "Disk IO"
 msgstr "디스크 IO"
 
-#: src/routes/index.ts:167 src/views/certificate/DNSCredential.vue:40
+#: src/routes/index.ts:178 src/views/certificate/DNSCredential.vue:40
 msgid "DNS Credentials"
 msgstr "DNS 인증 정보"
 
@@ -659,7 +687,7 @@ msgstr "로컬로 성공적으로 복제됨"
 msgid "Edit %{n}"
 msgstr "%{n} 편집"
 
-#: src/routes/index.ts:112 src/views/config/ConfigEdit.vue:81
+#: src/routes/index.ts:122 src/views/config/ConfigEditor.vue:187
 msgid "Edit Configuration"
 msgstr "구성 편집"
 
@@ -748,7 +776,12 @@ msgstr "성공적으로 활성화됨"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Let's Encrypt로 웹사이트 암호화"
 
-#: src/routes/index.ts:217 src/views/environment/Environment.vue:34
+#: src/views/config/ConfigList.vue:151
+#, fuzzy
+msgid "Enter"
+msgstr "간격"
+
+#: src/routes/index.ts:228 src/views/environment/Environment.vue:34
 msgid "Environment"
 msgstr "환경"
 
@@ -765,7 +798,7 @@ msgstr "환경"
 msgid "Error"
 msgstr "오류"
 
-#: src/routes/index.ts:200 src/views/domain/ngx_conf/LogEntry.vue:84
+#: src/routes/index.ts:211 src/views/domain/ngx_conf/LogEntry.vue:84
 msgid "Error Logs"
 msgstr "오류 로그"
 
@@ -814,7 +847,7 @@ msgstr "인증서 정보 가져오기 실패"
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr "저장 실패, 구성에서 구문 오류가 감지되었습니다."
 
-#: src/views/config/config.ts:20
+#: src/views/config/configColumns.ts:24
 msgid "File"
 msgstr "파일"
 
@@ -844,16 +877,16 @@ msgstr "완료됨"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr "중국 사용자를 위해: https://mirror.ghproxy.com/"
 
-#: src/views/config/ConfigEdit.vue:90
+#: src/views/config/ConfigEditor.vue:199
 msgid "Format Code"
 msgstr "코드 형식"
 
-#: src/views/config/ConfigEdit.vue:68
+#: src/views/config/ConfigEditor.vue:166
 #, fuzzy
 msgid "Format error %{msg}"
 msgstr "형식 오류 %{msg}"
 
-#: src/views/config/ConfigEdit.vue:66
+#: src/views/config/ConfigEditor.vue:164
 #, fuzzy
 msgid "Format successfully"
 msgstr "성공적으로 형식 지정됨"
@@ -934,7 +967,7 @@ msgstr ""
 msgid "Import"
 msgstr "가져오기"
 
-#: src/routes/index.ts:157 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:168 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Import Certificate"
 msgstr "인증서 상태"
@@ -965,7 +998,7 @@ msgstr ""
 msgid "Input the recovery code:"
 msgstr ""
 
-#: src/routes/index.ts:288 src/views/other/Install.vue:134
+#: src/routes/index.ts:299 src/views/other/Install.vue:134
 msgid "Install"
 msgstr "설치"
 
@@ -987,7 +1020,17 @@ msgstr "유효함"
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:60
+#: src/views/config/components/Rename.vue:62
+#: src/views/config/ConfigEditor.vue:233
+#, fuzzy
+msgid "Invalid filename"
+msgstr "Invalid E-mail!"
+
+#: src/views/config/components/Mkdir.vue:60
+msgid "Invalid folder name"
+msgstr ""
+
+#: src/components/OTP/useOTPModal.ts:64
 msgid "Invalid passcode or recovery code"
 msgstr ""
 
@@ -1092,7 +1135,7 @@ msgstr "위치들"
 msgid "Log"
 msgstr "로그인"
 
-#: src/routes/index.ts:294 src/views/other/Login.vue:192
+#: src/routes/index.ts:305 src/views/other/Login.vue:192
 msgid "Login"
 msgstr "로그인"
 
@@ -1132,7 +1175,8 @@ msgstr ""
 "인증서를 획득하기 전에 .well-known 디렉토리에 대한역방향 프록시를 "
 "HTTPChallengePort(기본값: 9180)로 구성했는지 확인하세요."
 
-#: src/routes/index.ts:102
+#: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
+#: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
 msgid "Manage Configs"
 msgstr "구성 관리"
 
@@ -1145,7 +1189,7 @@ msgstr "사이트 관리"
 msgid "Manage Streams"
 msgstr "스트림 관리"
 
-#: src/routes/index.ts:240 src/views/user/User.vue:9
+#: src/routes/index.ts:251 src/views/user/User.vue:9
 msgid "Manage Users"
 msgstr "사용자 관리"
 
@@ -1179,11 +1223,12 @@ msgstr "실행 모드"
 #: src/components/ChatGPT/ChatGPT.vue:248
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
+#: src/views/config/ConfigList.vue:151
 #, fuzzy
 msgid "Modify"
 msgstr "설정 수정"
 
-#: src/routes/index.ts:147 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:158 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "인증서 상태"
@@ -1205,7 +1250,9 @@ msgstr "단일 지시문"
 #: src/views/certificate/ACMEUser.vue:13
 #: src/views/certificate/CertificateEditor.vue:152
 #: src/views/certificate/CertificateList/certColumns.tsx:10
-#: src/views/certificate/DNSCredential.vue:11 src/views/config/config.ts:7
+#: src/views/certificate/DNSCredential.vue:11
+#: src/views/config/components/Mkdir.vue:67 src/views/config/configColumns.ts:8
+#: src/views/config/ConfigEditor.vue:239
 #: src/views/domain/components/RightSettings.vue:83
 #: src/views/domain/components/SiteDuplicate.vue:129
 #: src/views/domain/DomainList.vue:13
@@ -1233,6 +1280,16 @@ msgstr "네트워크 총 수신"
 msgid "Network Total Send"
 msgstr "네트워크 총 송신"
 
+#: src/views/config/components/Rename.vue:70
+#, fuzzy
+msgid "New name"
+msgstr "이름 변경"
+
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "New Path"
+msgstr "경로"
+
 #: src/views/system/Upgrade.vue:210
 msgid "New version released"
 msgstr "새 버전 출시"
@@ -1264,7 +1321,7 @@ msgstr "Nginx 제어"
 msgid "Nginx Error Log Path"
 msgstr "Nginx 오류 로그 경로"
 
-#: src/routes/index.ts:185 src/views/nginx_log/NginxLog.vue:143
+#: src/routes/index.ts:196 src/views/nginx_log/NginxLog.vue:143
 msgid "Nginx Log"
 msgstr "Nginx 로그"
 
@@ -1301,7 +1358,7 @@ msgstr "노드 시크릿"
 msgid "Not After"
 msgstr "만료일"
 
-#: src/routes/index.ts:300
+#: src/routes/index.ts:311
 msgid "Not Found"
 msgstr "찾을 수 없음"
 
@@ -1319,7 +1376,7 @@ msgstr "참고"
 msgid "Notification"
 msgstr "알림"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:231
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:242
 #, fuzzy
 msgid "Notifications"
 msgstr "알림"
@@ -1389,6 +1446,10 @@ msgstr "온라인"
 msgid "OpenAI"
 msgstr "오픈AI"
 
+#: src/views/config/components/Rename.vue:66
+msgid "Original name"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:177
 #, fuzzy
 msgid "OS"
@@ -1420,7 +1481,7 @@ msgstr "비밀번호"
 msgid "Password (*)"
 msgstr "비밀번호 (*)"
 
-#: src/views/config/ConfigEdit.vue:118
+#: src/views/config/ConfigEditor.vue:245
 #: src/views/domain/ngx_conf/LocationEditor.vue:118
 #: src/views/domain/ngx_conf/LocationEditor.vue:90
 msgid "Path"
@@ -1460,6 +1521,17 @@ msgstr ""
 "먼저 인증서 > DNS 자격 증명에 자격 증명을 추가한 다음,DNS 제공자의 API를 요청"
 "하려면 아래 자격 증명 중 하나를 선택해주세요."
 
+#: src/views/config/components/Rename.vue:61
+#: src/views/config/ConfigEditor.vue:232
+#, fuzzy
+msgid "Please input a filename"
+msgstr "사용자 이름을 입력해주세요!"
+
+#: src/views/config/components/Mkdir.vue:59
+#, fuzzy
+msgid "Please input a folder name"
+msgstr "사용자 이름을 입력해주세요!"
+
 #: src/views/domain/components/SiteDuplicate.vue:38
 #: src/views/stream/components/StreamDuplicate.vue:38
 msgid ""
@@ -1495,7 +1567,7 @@ msgstr "적어도 하나의 노드를 선택해주세요!"
 msgid "Pre-release"
 msgstr "사전 출시"
 
-#: src/routes/index.ts:249 src/views/preference/Preference.vue:105
+#: src/routes/index.ts:260 src/views/preference/Preference.vue:105
 msgid "Preference"
 msgstr "환경설정"
 
@@ -1620,11 +1692,18 @@ msgstr "성공적으로 제거됨"
 msgid "Removed successfully"
 msgstr "성공적으로 제거됨"
 
+#: src/views/config/components/Rename.vue:52
+#: src/views/config/ConfigList.vue:159
 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
 #, fuzzy
 msgid "Rename"
 msgstr "이름 변경"
 
+#: src/views/config/components/Rename.vue:37
+#, fuzzy
+msgid "Rename successfully"
+msgstr "성공적으로 갱신됨"
+
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1679,7 +1758,7 @@ msgstr "실행 중"
 
 #: src/components/ChatGPT/ChatGPT.vue:251
 #: src/views/certificate/CertificateEditor.vue:254
-#: src/views/config/ConfigEdit.vue:96 src/views/domain/DomainEdit.vue:260
+#: src/views/config/ConfigEditor.vue:205 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/preference/Preference.vue:145 src/views/stream/StreamEdit.vue:252
 msgid "Save"
@@ -1689,7 +1768,7 @@ msgstr "저장"
 msgid "Save Directive"
 msgstr "지시문 저장"
 
-#: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:46
+#: src/views/config/ConfigEditor.vue:154 src/views/domain/DomainAdd.vue:46
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
 msgid "Save error %{msg}"
 msgstr "저장 오류 %{msg}"
@@ -1702,7 +1781,7 @@ msgstr "저장 오류 %{msg}"
 msgid "Save successfully"
 msgstr "성공적으로 저장됨"
 
-#: src/views/config/ConfigEdit.vue:55 src/views/domain/DomainAdd.vue:37
+#: src/views/config/ConfigEditor.vue:150 src/views/domain/DomainAdd.vue:37
 #: src/views/domain/DomainEdit.vue:143
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
 #: src/views/stream/StreamEdit.vue:138
@@ -1733,7 +1812,9 @@ msgstr "보내기"
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:42
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:213
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:253
-#: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81
+#: src/views/config/components/Mkdir.vue:38
+#: src/views/config/components/Rename.vue:40
+#: src/views/config/ConfigEditor.vue:93 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
@@ -1787,7 +1868,7 @@ msgstr ""
 msgid "Single Directive"
 msgstr "단일 지시문"
 
-#: src/routes/index.ts:207
+#: src/routes/index.ts:218
 #, fuzzy
 msgid "Site Logs"
 msgstr "사이트 로그"
@@ -1896,7 +1977,7 @@ msgstr "인증서 갱신 성공"
 msgid "Sync to"
 msgstr ""
 
-#: src/routes/index.ts:258
+#: src/routes/index.ts:269
 msgid "System"
 msgstr "시스템"
 
@@ -1909,7 +1990,7 @@ msgstr ""
 msgid "Target"
 msgstr "대상"
 
-#: src/routes/index.ts:177 src/views/pty/Terminal.vue:114
+#: src/routes/index.ts:188 src/views/pty/Terminal.vue:114
 msgid "Terminal"
 msgstr "터미널"
 
@@ -2065,18 +2146,19 @@ msgstr ""
 msgid "Trash"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:66
+#: src/components/OTP/useOTPModal.ts:70
 msgid "Two-factor authentication required"
 msgstr ""
 
 #: src/views/certificate/CertificateList/certColumns.tsx:25
-#: src/views/config/config.ts:12 src/views/notification/Notification.vue:13
+#: src/views/config/configColumns.ts:16
+#: src/views/notification/Notification.vue:13
 msgid "Type"
 msgstr "유형"
 
 #: src/views/certificate/ACMEUser.vue:53
-#: src/views/certificate/DNSCredential.vue:27 src/views/config/config.ts:27
-#: src/views/config/ConfigEdit.vue:121
+#: src/views/certificate/DNSCredential.vue:27
+#: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:258
 #: src/views/domain/components/RightSettings.vue:86
 #: src/views/domain/DomainList.vue:41 src/views/environment/envColumns.tsx:124
 #: src/views/stream/components/RightSettings.vue:85
@@ -2089,7 +2171,7 @@ msgstr "업데이트됨"
 msgid "Updated successfully"
 msgstr "성공적으로 저장되었습니다"
 
-#: src/routes/index.ts:273 src/views/environment/Environment.vue:50
+#: src/routes/index.ts:284 src/views/environment/Environment.vue:50
 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228
 msgid "Upgrade"
 msgstr "업그레이드"
@@ -2225,6 +2307,10 @@ msgstr "최신 버전을 사용하고 있습니다"
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "이 페이지에서 Nginx UI 업그레이드를 확인할 수 있습니다."
 
+#, fuzzy
+#~ msgid "Rename "
+#~ msgstr "이름 변경"
+
 #~ msgid "Auto Cert"
 #~ msgstr "자동 인증"
 
@@ -2284,6 +2370,3 @@ msgstr "이 페이지에서 Nginx UI 업그레이드를 확인할 수 있습니
 
 #~ msgid "404 Not Found"
 #~ msgstr "404 Not Found"
-
-#~ msgid "Invalid E-mail!"
-#~ msgstr "Invalid E-mail!"

+ 119 - 44
app/src/language/messages.pot

@@ -10,16 +10,16 @@ msgstr ""
 msgid "2FA Settings"
 msgstr ""
 
-#: src/routes/index.ts:266
+#: src/routes/index.ts:277
 msgid "About"
 msgstr ""
 
-#: src/routes/index.ts:193
+#: src/routes/index.ts:204
 #: src/views/domain/ngx_conf/LogEntry.vue:76
 msgid "Access Logs"
 msgstr ""
 
-#: src/routes/index.ts:131
+#: src/routes/index.ts:142
 #: src/views/certificate/ACMEUser.vue:76
 #: src/views/certificate/ACMEUserSelector.vue:85
 msgid "ACME User"
@@ -28,7 +28,7 @@ msgstr ""
 #: src/views/certificate/ACMEUser.vue:59
 #: src/views/certificate/CertificateList/certColumns.tsx:89
 #: src/views/certificate/DNSCredential.vue:33
-#: src/views/config/config.ts:34
+#: src/views/config/configColumns.ts:38
 #: src/views/domain/DomainList.vue:47
 #: src/views/environment/envColumns.tsx:131
 #: src/views/notification/Notification.vue:37
@@ -48,6 +48,12 @@ msgstr ""
 msgid "Add"
 msgstr ""
 
+#: src/routes/index.ts:112
+#: src/views/config/ConfigEditor.vue:128
+#: src/views/config/ConfigEditor.vue:187
+msgid "Add Configuration"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:95
 msgid "Add Directive Below"
 msgstr ""
@@ -175,8 +181,9 @@ msgid "Auto-renewal enabled for %{name}"
 msgstr ""
 
 #: src/views/certificate/CertificateEditor.vue:247
-#: src/views/config/Config.vue:71
-#: src/views/config/ConfigEdit.vue:87
+#: src/views/config/ConfigEditor.vue:196
+#: src/views/config/ConfigList.vue:173
+#: src/views/config/ConfigList.vue:99
 #: src/views/domain/DomainEdit.vue:253
 #: src/views/nginx_log/NginxLog.vue:168
 #: src/views/stream/StreamEdit.vue:245
@@ -207,7 +214,7 @@ msgstr ""
 msgid "Base information"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:115
+#: src/views/config/ConfigEditor.vue:224
 #: src/views/domain/components/RightSettings.vue:75
 #: src/views/preference/Preference.vue:110
 #: src/views/stream/components/RightSettings.vue:74
@@ -266,12 +273,12 @@ msgid_plural "Certificates Status"
 msgstr[0] ""
 msgstr[1] ""
 
-#: src/routes/index.ts:122
+#: src/routes/index.ts:133
 #: src/views/certificate/CertificateList/Certificate.vue:13
 msgid "Certificates"
 msgstr ""
 
-#: src/routes/index.ts:139
+#: src/routes/index.ts:150
 msgid "Certificates List"
 msgstr ""
 
@@ -290,6 +297,10 @@ msgid_plural "Changed Certificates"
 msgstr[0] ""
 msgstr[1] ""
 
+#: src/views/config/ConfigEditor.vue:251
+msgid "Changed Path"
+msgstr ""
+
 #: src/views/environment/BatchUpgrader.vue:161
 #: src/views/system/Upgrade.vue:190
 msgid "Channel"
@@ -337,7 +348,7 @@ msgstr ""
 msgid "Configuration Name"
 msgstr ""
 
-#: src/views/config/Config.vue:42
+#: src/views/config/ConfigList.vue:91
 msgid "Configurations"
 msgstr ""
 
@@ -387,11 +398,24 @@ msgstr ""
 msgid "Create Another"
 msgstr ""
 
+#: src/views/config/ConfigList.vue:109
+msgid "Create File"
+msgstr ""
+
+#: src/views/config/components/Mkdir.vue:50
+#: src/views/config/ConfigList.vue:116
+msgid "Create Folder"
+msgstr ""
+
 #: src/views/notification/Notification.vue:31
 #: src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgstr ""
 
+#: src/views/config/components/Mkdir.vue:35
+msgid "Created successfully"
+msgstr ""
+
 #: src/language/constants.ts:9
 msgid "Creating client facilitates communication with the CA server"
 msgstr ""
@@ -426,6 +450,9 @@ msgid "Customize the name of local server to be displayed in the environment ind
 msgstr ""
 
 #: src/routes/index.ts:39
+#: src/views/config/ConfigEditor.vue:118
+#: src/views/config/ConfigEditor.vue:79
+#: src/views/config/ConfigList.vue:57
 msgid "Dashboard"
 msgstr ""
 
@@ -504,7 +531,7 @@ msgstr ""
 msgid "Directives"
 msgstr ""
 
-#: src/views/config/config.ts:18
+#: src/views/config/configColumns.ts:22
 msgid "Directory"
 msgstr ""
 
@@ -538,7 +565,7 @@ msgstr ""
 msgid "Disk IO"
 msgstr ""
 
-#: src/routes/index.ts:167
+#: src/routes/index.ts:178
 #: src/views/certificate/DNSCredential.vue:40
 msgid "DNS Credentials"
 msgstr ""
@@ -652,8 +679,8 @@ msgstr ""
 msgid "Edit %{n}"
 msgstr ""
 
-#: src/routes/index.ts:112
-#: src/views/config/ConfigEdit.vue:81
+#: src/routes/index.ts:122
+#: src/views/config/ConfigEditor.vue:187
 msgid "Edit Configuration"
 msgstr ""
 
@@ -742,7 +769,11 @@ msgstr ""
 msgid "Encrypt website with Let's Encrypt"
 msgstr ""
 
-#: src/routes/index.ts:217
+#: src/views/config/ConfigList.vue:151
+msgid "Enter"
+msgstr ""
+
+#: src/routes/index.ts:228
 #: src/views/environment/Environment.vue:34
 msgid "Environment"
 msgstr ""
@@ -760,7 +791,7 @@ msgstr ""
 msgid "Error"
 msgstr ""
 
-#: src/routes/index.ts:200
+#: src/routes/index.ts:211
 #: src/views/domain/ngx_conf/LogEntry.vue:84
 msgid "Error Logs"
 msgstr ""
@@ -809,7 +840,7 @@ msgstr ""
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr ""
 
-#: src/views/config/config.ts:20
+#: src/views/config/configColumns.ts:24
 msgid "File"
 msgstr ""
 
@@ -839,15 +870,15 @@ msgstr ""
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:90
+#: src/views/config/ConfigEditor.vue:199
 msgid "Format Code"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:68
+#: src/views/config/ConfigEditor.vue:166
 msgid "Format error %{msg}"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:66
+#: src/views/config/ConfigEditor.vue:164
 msgid "Format successfully"
 msgstr ""
 
@@ -920,7 +951,7 @@ msgstr ""
 msgid "Import"
 msgstr ""
 
-#: src/routes/index.ts:157
+#: src/routes/index.ts:168
 #: src/views/certificate/CertificateEditor.vue:79
 msgid "Import Certificate"
 msgstr ""
@@ -950,7 +981,7 @@ msgstr ""
 msgid "Input the recovery code:"
 msgstr ""
 
-#: src/routes/index.ts:288
+#: src/routes/index.ts:299
 #: src/views/other/Install.vue:134
 msgid "Install"
 msgstr ""
@@ -971,7 +1002,16 @@ msgstr ""
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:60
+#: src/views/config/components/Rename.vue:62
+#: src/views/config/ConfigEditor.vue:233
+msgid "Invalid filename"
+msgstr ""
+
+#: src/views/config/components/Mkdir.vue:60
+msgid "Invalid folder name"
+msgstr ""
+
+#: src/components/OTP/useOTPModal.ts:64
 msgid "Invalid passcode or recovery code"
 msgstr ""
 
@@ -1067,7 +1107,7 @@ msgstr ""
 msgid "Log"
 msgstr ""
 
-#: src/routes/index.ts:294
+#: src/routes/index.ts:305
 #: src/views/other/Login.vue:192
 msgid "Login"
 msgstr ""
@@ -1094,6 +1134,9 @@ msgid "Make sure you have configured a reverse proxy for .well-known directory t
 msgstr ""
 
 #: src/routes/index.ts:102
+#: src/views/config/ConfigEditor.vue:123
+#: src/views/config/ConfigEditor.vue:84
+#: src/views/config/ConfigList.vue:62
 msgid "Manage Configs"
 msgstr ""
 
@@ -1107,7 +1150,7 @@ msgstr ""
 msgid "Manage Streams"
 msgstr ""
 
-#: src/routes/index.ts:240
+#: src/routes/index.ts:251
 #: src/views/user/User.vue:9
 msgid "Manage Users"
 msgstr ""
@@ -1140,10 +1183,11 @@ msgstr ""
 #: src/components/ChatGPT/ChatGPT.vue:248
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
+#: src/views/config/ConfigList.vue:151
 msgid "Modify"
 msgstr ""
 
-#: src/routes/index.ts:147
+#: src/routes/index.ts:158
 #: src/views/certificate/CertificateEditor.vue:79
 msgid "Modify Certificate"
 msgstr ""
@@ -1164,7 +1208,9 @@ msgstr ""
 #: src/views/certificate/CertificateEditor.vue:152
 #: src/views/certificate/CertificateList/certColumns.tsx:10
 #: src/views/certificate/DNSCredential.vue:11
-#: src/views/config/config.ts:7
+#: src/views/config/components/Mkdir.vue:67
+#: src/views/config/configColumns.ts:8
+#: src/views/config/ConfigEditor.vue:239
 #: src/views/domain/components/RightSettings.vue:83
 #: src/views/domain/components/SiteDuplicate.vue:129
 #: src/views/domain/DomainList.vue:13
@@ -1193,6 +1239,14 @@ msgstr ""
 msgid "Network Total Send"
 msgstr ""
 
+#: src/views/config/components/Rename.vue:70
+msgid "New name"
+msgstr ""
+
+#: src/views/config/ConfigEditor.vue:251
+msgid "New Path"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:210
 msgid "New version released"
 msgstr ""
@@ -1224,7 +1278,7 @@ msgstr ""
 msgid "Nginx Error Log Path"
 msgstr ""
 
-#: src/routes/index.ts:185
+#: src/routes/index.ts:196
 #: src/views/nginx_log/NginxLog.vue:143
 msgid "Nginx Log"
 msgstr ""
@@ -1260,7 +1314,7 @@ msgstr ""
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:300
+#: src/routes/index.ts:311
 msgid "Not Found"
 msgstr ""
 
@@ -1278,7 +1332,7 @@ msgid "Notification"
 msgstr ""
 
 #: src/components/Notification/Notification.vue:82
-#: src/routes/index.ts:231
+#: src/routes/index.ts:242
 msgid "Notifications"
 msgstr ""
 
@@ -1344,6 +1398,10 @@ msgstr ""
 msgid "OpenAI"
 msgstr ""
 
+#: src/views/config/components/Rename.vue:66
+msgid "Original name"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:177
 msgid "OS"
 msgstr ""
@@ -1375,7 +1433,7 @@ msgstr ""
 msgid "Password (*)"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:118
+#: src/views/config/ConfigEditor.vue:245
 #: src/views/domain/ngx_conf/LocationEditor.vue:118
 #: src/views/domain/ngx_conf/LocationEditor.vue:90
 msgid "Path"
@@ -1409,6 +1467,15 @@ msgstr ""
 msgid "Please first add credentials in Certification > DNS Credentials, and then select one of the credentialsbelow to request the API of the DNS provider."
 msgstr ""
 
+#: src/views/config/components/Rename.vue:61
+#: src/views/config/ConfigEditor.vue:232
+msgid "Please input a filename"
+msgstr ""
+
+#: src/views/config/components/Mkdir.vue:59
+msgid "Please input a folder name"
+msgstr ""
+
 #: src/views/domain/components/SiteDuplicate.vue:38
 #: src/views/stream/components/StreamDuplicate.vue:38
 msgid "Please input name, this will be used as the filename of the new configuration!"
@@ -1444,7 +1511,7 @@ msgstr ""
 msgid "Pre-release"
 msgstr ""
 
-#: src/routes/index.ts:249
+#: src/routes/index.ts:260
 #: src/views/preference/Preference.vue:105
 msgid "Preference"
 msgstr ""
@@ -1561,10 +1628,16 @@ msgstr ""
 msgid "Removed successfully"
 msgstr ""
 
+#: src/views/config/components/Rename.vue:52
+#: src/views/config/ConfigList.vue:159
 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
 msgid "Rename"
 msgstr ""
 
+#: src/views/config/components/Rename.vue:37
+msgid "Rename successfully"
+msgstr ""
+
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 msgid "Renew Certificate"
@@ -1613,7 +1686,7 @@ msgstr ""
 
 #: src/components/ChatGPT/ChatGPT.vue:251
 #: src/views/certificate/CertificateEditor.vue:254
-#: src/views/config/ConfigEdit.vue:96
+#: src/views/config/ConfigEditor.vue:205
 #: src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/preference/Preference.vue:145
@@ -1625,7 +1698,7 @@ msgstr ""
 msgid "Save Directive"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:57
+#: src/views/config/ConfigEditor.vue:154
 #: src/views/domain/DomainAdd.vue:46
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
 msgid "Save error %{msg}"
@@ -1638,7 +1711,7 @@ msgstr ""
 msgid "Save successfully"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:55
+#: src/views/config/ConfigEditor.vue:150
 #: src/views/domain/DomainAdd.vue:37
 #: src/views/domain/DomainEdit.vue:143
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
@@ -1670,7 +1743,9 @@ msgstr ""
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:42
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:213
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:253
-#: src/views/config/ConfigEdit.vue:40
+#: src/views/config/components/Mkdir.vue:38
+#: src/views/config/components/Rename.vue:40
+#: src/views/config/ConfigEditor.vue:93
 #: src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15
@@ -1725,7 +1800,7 @@ msgstr ""
 msgid "Single Directive"
 msgstr ""
 
-#: src/routes/index.ts:207
+#: src/routes/index.ts:218
 msgid "Site Logs"
 msgstr ""
 
@@ -1822,7 +1897,7 @@ msgstr ""
 msgid "Sync to"
 msgstr ""
 
-#: src/routes/index.ts:258
+#: src/routes/index.ts:269
 msgid "System"
 msgstr ""
 
@@ -1835,7 +1910,7 @@ msgstr ""
 msgid "Target"
 msgstr ""
 
-#: src/routes/index.ts:177
+#: src/routes/index.ts:188
 #: src/views/pty/Terminal.vue:114
 msgid "Terminal"
 msgstr ""
@@ -1957,20 +2032,20 @@ msgstr ""
 msgid "Trash"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:66
+#: src/components/OTP/useOTPModal.ts:70
 msgid "Two-factor authentication required"
 msgstr ""
 
 #: src/views/certificate/CertificateList/certColumns.tsx:25
-#: src/views/config/config.ts:12
+#: src/views/config/configColumns.ts:16
 #: src/views/notification/Notification.vue:13
 msgid "Type"
 msgstr ""
 
 #: src/views/certificate/ACMEUser.vue:53
 #: src/views/certificate/DNSCredential.vue:27
-#: src/views/config/config.ts:27
-#: src/views/config/ConfigEdit.vue:121
+#: src/views/config/configColumns.ts:31
+#: src/views/config/ConfigEditor.vue:258
 #: src/views/domain/components/RightSettings.vue:86
 #: src/views/domain/DomainList.vue:41
 #: src/views/environment/envColumns.tsx:124
@@ -1984,7 +2059,7 @@ msgstr ""
 msgid "Updated successfully"
 msgstr ""
 
-#: src/routes/index.ts:273
+#: src/routes/index.ts:284
 #: src/views/environment/Environment.vue:50
 #: src/views/system/Upgrade.vue:145
 #: src/views/system/Upgrade.vue:228

+ 132 - 50
app/src/language/ru_RU/app.po

@@ -17,15 +17,15 @@ msgstr ""
 msgid "2FA Settings"
 msgstr ""
 
-#: src/routes/index.ts:266
+#: src/routes/index.ts:277
 msgid "About"
 msgstr "О проекте"
 
-#: src/routes/index.ts:193 src/views/domain/ngx_conf/LogEntry.vue:76
+#: src/routes/index.ts:204 src/views/domain/ngx_conf/LogEntry.vue:76
 msgid "Access Logs"
 msgstr "Журнал доступа"
 
-#: src/routes/index.ts:131 src/views/certificate/ACMEUser.vue:76
+#: src/routes/index.ts:142 src/views/certificate/ACMEUser.vue:76
 #: src/views/certificate/ACMEUserSelector.vue:85
 #, fuzzy
 msgid "ACME User"
@@ -33,8 +33,9 @@ msgstr "Пользователь"
 
 #: src/views/certificate/ACMEUser.vue:59
 #: src/views/certificate/CertificateList/certColumns.tsx:89
-#: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34
-#: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131
+#: src/views/certificate/DNSCredential.vue:33
+#: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
+#: src/views/environment/envColumns.tsx:131
 #: src/views/notification/Notification.vue:37
 #: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
@@ -51,6 +52,12 @@ msgstr "Действие"
 msgid "Add"
 msgstr "Добавить"
 
+#: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:128
+#: src/views/config/ConfigEditor.vue:187
+#, fuzzy
+msgid "Add Configuration"
+msgstr "Редактировать Конфигурацию"
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:95
 msgid "Add Directive Below"
 msgstr "Добавить директиву ниже"
@@ -188,9 +195,9 @@ msgid "Auto-renewal enabled for %{name}"
 msgstr "Автообновление включено для %{name}"
 
 #: src/views/certificate/CertificateEditor.vue:247
-#: src/views/config/Config.vue:71 src/views/config/ConfigEdit.vue:87
-#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168
-#: src/views/stream/StreamEdit.vue:245
+#: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
+#: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
+#: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
 msgid "Back"
 msgstr "Назад"
 
@@ -219,7 +226,7 @@ msgstr ""
 msgid "Base information"
 msgstr "Основная информация"
 
-#: src/views/config/ConfigEdit.vue:115
+#: src/views/config/ConfigEditor.vue:224
 #: src/views/domain/components/RightSettings.vue:75
 #: src/views/preference/Preference.vue:110
 #: src/views/stream/components/RightSettings.vue:74
@@ -282,13 +289,13 @@ msgid_plural "Certificates Status"
 msgstr[0] "Статус сертификата"
 msgstr[1] "Статус сертификата"
 
-#: src/routes/index.ts:122
+#: src/routes/index.ts:133
 #: src/views/certificate/CertificateList/Certificate.vue:13
 #, fuzzy
 msgid "Certificates"
 msgstr "Статус сертификата"
 
-#: src/routes/index.ts:139
+#: src/routes/index.ts:150
 #, fuzzy
 msgid "Certificates List"
 msgstr "Список"
@@ -310,6 +317,11 @@ msgid_plural "Changed Certificates"
 msgstr[0] "Сертификат действителен"
 msgstr[1] "Сертификат действителен"
 
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "Changed Path"
+msgstr "Сертификат действителен"
+
 #: src/views/environment/BatchUpgrader.vue:161 src/views/system/Upgrade.vue:190
 msgid "Channel"
 msgstr "Канал"
@@ -359,7 +371,7 @@ msgstr "Проверка конфигурации успешна"
 msgid "Configuration Name"
 msgstr "Название конфигурации"
 
-#: src/views/config/Config.vue:42
+#: src/views/config/ConfigList.vue:91
 msgid "Configurations"
 msgstr "Конфигурации"
 
@@ -410,10 +422,25 @@ msgstr "Создан в"
 msgid "Create Another"
 msgstr "Создать еще"
 
+#: src/views/config/ConfigList.vue:109
+#, fuzzy
+msgid "Create File"
+msgstr "Создан в"
+
+#: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
+#, fuzzy
+msgid "Create Folder"
+msgstr "Создать еще"
+
 #: src/views/notification/Notification.vue:31 src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgstr "Создан в"
 
+#: src/views/config/components/Mkdir.vue:35
+#, fuzzy
+msgid "Created successfully"
+msgstr "Отключено успешно"
+
 #: src/language/constants.ts:9
 msgid "Creating client facilitates communication with the CA server"
 msgstr ""
@@ -449,7 +476,8 @@ msgid ""
 "indicator."
 msgstr ""
 
-#: src/routes/index.ts:39
+#: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
+#: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
 msgid "Dashboard"
 msgstr "Доска"
 
@@ -530,7 +558,7 @@ msgstr "Деректива"
 msgid "Directives"
 msgstr "Дерективы"
 
-#: src/views/config/config.ts:18
+#: src/views/config/configColumns.ts:22
 #, fuzzy
 msgid "Directory"
 msgstr "Деректива"
@@ -562,7 +590,7 @@ msgstr "Отключено успешно"
 msgid "Disk IO"
 msgstr "Нагрузка на Диск IO"
 
-#: src/routes/index.ts:167 src/views/certificate/DNSCredential.vue:40
+#: src/routes/index.ts:178 src/views/certificate/DNSCredential.vue:40
 msgid "DNS Credentials"
 msgstr ""
 
@@ -687,7 +715,7 @@ msgstr "Saved successfully"
 msgid "Edit %{n}"
 msgstr "Редактировать %{n}"
 
-#: src/routes/index.ts:112 src/views/config/ConfigEdit.vue:81
+#: src/routes/index.ts:122 src/views/config/ConfigEditor.vue:187
 msgid "Edit Configuration"
 msgstr "Редактировать Конфигурацию"
 
@@ -779,7 +807,11 @@ msgstr "Активировано успешно"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Использовать для сайта Let's Encrypt"
 
-#: src/routes/index.ts:217 src/views/environment/Environment.vue:34
+#: src/views/config/ConfigList.vue:151
+msgid "Enter"
+msgstr ""
+
+#: src/routes/index.ts:228 src/views/environment/Environment.vue:34
 msgid "Environment"
 msgstr "Окружение"
 
@@ -797,7 +829,7 @@ msgstr "Комментарии"
 msgid "Error"
 msgstr "Ошибка"
 
-#: src/routes/index.ts:200 src/views/domain/ngx_conf/LogEntry.vue:84
+#: src/routes/index.ts:211 src/views/domain/ngx_conf/LogEntry.vue:84
 msgid "Error Logs"
 msgstr "Ошибка логирования"
 
@@ -846,7 +878,7 @@ msgstr "Не удалось получить информацию о серти
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr "Не удалось сохранить, обнаружены синтаксические ошибки в конфигурации."
 
-#: src/views/config/config.ts:20
+#: src/views/config/configColumns.ts:24
 msgid "File"
 msgstr "Файл"
 
@@ -876,16 +908,16 @@ msgstr "Готово"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:90
+#: src/views/config/ConfigEditor.vue:199
 msgid "Format Code"
 msgstr "Форматировать код"
 
-#: src/views/config/ConfigEdit.vue:68
+#: src/views/config/ConfigEditor.vue:166
 #, fuzzy
 msgid "Format error %{msg}"
 msgstr "Ошибка форматирования %{msg}"
 
-#: src/views/config/ConfigEdit.vue:66
+#: src/views/config/ConfigEditor.vue:164
 #, fuzzy
 msgid "Format successfully"
 msgstr "Форматирование успешно"
@@ -967,7 +999,7 @@ msgstr ""
 msgid "Import"
 msgstr "Экспорт"
 
-#: src/routes/index.ts:157 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:168 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Import Certificate"
 msgstr "Статус сертификата"
@@ -998,7 +1030,7 @@ msgstr ""
 msgid "Input the recovery code:"
 msgstr ""
 
-#: src/routes/index.ts:288 src/views/other/Install.vue:134
+#: src/routes/index.ts:299 src/views/other/Install.vue:134
 msgid "Install"
 msgstr "Установить"
 
@@ -1020,7 +1052,17 @@ msgstr "Действительный"
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:60
+#: src/views/config/components/Rename.vue:62
+#: src/views/config/ConfigEditor.vue:233
+#, fuzzy
+msgid "Invalid filename"
+msgstr "Invalid E-mail!"
+
+#: src/views/config/components/Mkdir.vue:60
+msgid "Invalid folder name"
+msgstr ""
+
+#: src/components/OTP/useOTPModal.ts:64
 msgid "Invalid passcode or recovery code"
 msgstr ""
 
@@ -1126,7 +1168,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "Логин"
 
-#: src/routes/index.ts:294 src/views/other/Login.vue:192
+#: src/routes/index.ts:305 src/views/other/Login.vue:192
 msgid "Login"
 msgstr "Логин"
 
@@ -1161,7 +1203,8 @@ msgstr ""
 "Убедитесь, что вы настроили обратный прокси-сервер для каталога .well-known "
 "на HTTPChallengePort перед получением сертификата»."
 
-#: src/routes/index.ts:102
+#: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
+#: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
 msgid "Manage Configs"
 msgstr "Конфигурации"
 
@@ -1174,7 +1217,7 @@ msgstr "Сайты"
 msgid "Manage Streams"
 msgstr "Управление потоками"
 
-#: src/routes/index.ts:240 src/views/user/User.vue:9
+#: src/routes/index.ts:251 src/views/user/User.vue:9
 msgid "Manage Users"
 msgstr "Пользователи"
 
@@ -1208,11 +1251,12 @@ msgstr "Расширенный режим"
 #: src/components/ChatGPT/ChatGPT.vue:248
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
+#: src/views/config/ConfigList.vue:151
 #, fuzzy
 msgid "Modify"
 msgstr "Изменить"
 
-#: src/routes/index.ts:147 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:158 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "Статус сертификата"
@@ -1234,7 +1278,9 @@ msgstr "Одиночная директива"
 #: src/views/certificate/ACMEUser.vue:13
 #: src/views/certificate/CertificateEditor.vue:152
 #: src/views/certificate/CertificateList/certColumns.tsx:10
-#: src/views/certificate/DNSCredential.vue:11 src/views/config/config.ts:7
+#: src/views/certificate/DNSCredential.vue:11
+#: src/views/config/components/Mkdir.vue:67 src/views/config/configColumns.ts:8
+#: src/views/config/ConfigEditor.vue:239
 #: src/views/domain/components/RightSettings.vue:83
 #: src/views/domain/components/SiteDuplicate.vue:129
 #: src/views/domain/DomainList.vue:13
@@ -1262,6 +1308,16 @@ msgstr "Всего получено"
 msgid "Network Total Send"
 msgstr "Всего отправлено"
 
+#: src/views/config/components/Rename.vue:70
+#, fuzzy
+msgid "New name"
+msgstr "Имя пользователя"
+
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "New Path"
+msgstr "Путь"
+
 #: src/views/system/Upgrade.vue:210
 msgid "New version released"
 msgstr "Вышла новая версия"
@@ -1294,7 +1350,7 @@ msgstr "Управление Nginx"
 msgid "Nginx Error Log Path"
 msgstr "Путь для Nginx Error Log"
 
-#: src/routes/index.ts:185 src/views/nginx_log/NginxLog.vue:143
+#: src/routes/index.ts:196 src/views/nginx_log/NginxLog.vue:143
 msgid "Nginx Log"
 msgstr "Журнал"
 
@@ -1331,7 +1387,7 @@ msgstr ""
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:300
+#: src/routes/index.ts:311
 msgid "Not Found"
 msgstr "Не найден"
 
@@ -1349,7 +1405,7 @@ msgstr "Заметка"
 msgid "Notification"
 msgstr "Сертификат"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:231
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:242
 #, fuzzy
 msgid "Notifications"
 msgstr "Уведомления"
@@ -1419,6 +1475,10 @@ msgstr ""
 msgid "OpenAI"
 msgstr ""
 
+#: src/views/config/components/Rename.vue:66
+msgid "Original name"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:177
 #, fuzzy
 msgid "OS"
@@ -1450,7 +1510,7 @@ msgstr "Пароль"
 msgid "Password (*)"
 msgstr "Пароль (*)"
 
-#: src/views/config/ConfigEdit.vue:118
+#: src/views/config/ConfigEditor.vue:245
 #: src/views/domain/ngx_conf/LocationEditor.vue:118
 #: src/views/domain/ngx_conf/LocationEditor.vue:90
 msgid "Path"
@@ -1488,6 +1548,17 @@ msgid ""
 "select one of the credentialsbelow to request the API of the DNS provider."
 msgstr ""
 
+#: src/views/config/components/Rename.vue:61
+#: src/views/config/ConfigEditor.vue:232
+#, fuzzy
+msgid "Please input a filename"
+msgstr "Введите ваше имя пользователя!"
+
+#: src/views/config/components/Mkdir.vue:59
+#, fuzzy
+msgid "Please input a folder name"
+msgstr "Введите ваше имя пользователя!"
+
 #: src/views/domain/components/SiteDuplicate.vue:38
 #: src/views/stream/components/StreamDuplicate.vue:38
 msgid ""
@@ -1525,7 +1596,7 @@ msgstr ""
 msgid "Pre-release"
 msgstr ""
 
-#: src/routes/index.ts:249 src/views/preference/Preference.vue:105
+#: src/routes/index.ts:260 src/views/preference/Preference.vue:105
 msgid "Preference"
 msgstr "Настройки"
 
@@ -1650,11 +1721,18 @@ msgstr "Успешно сохранено"
 msgid "Removed successfully"
 msgstr "Успешно сохранено"
 
+#: src/views/config/components/Rename.vue:52
+#: src/views/config/ConfigList.vue:159
 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
 #, fuzzy
 msgid "Rename"
 msgstr "Имя пользователя"
 
+#: src/views/config/components/Rename.vue:37
+#, fuzzy
+msgid "Rename successfully"
+msgstr "Активировано успешно"
+
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1709,7 +1787,7 @@ msgstr "Выполняется"
 
 #: src/components/ChatGPT/ChatGPT.vue:251
 #: src/views/certificate/CertificateEditor.vue:254
-#: src/views/config/ConfigEdit.vue:96 src/views/domain/DomainEdit.vue:260
+#: src/views/config/ConfigEditor.vue:205 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/preference/Preference.vue:145 src/views/stream/StreamEdit.vue:252
 msgid "Save"
@@ -1719,7 +1797,7 @@ msgstr "Сохранить"
 msgid "Save Directive"
 msgstr "Сохранить директиву"
 
-#: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:46
+#: src/views/config/ConfigEditor.vue:154 src/views/domain/DomainAdd.vue:46
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
 msgid "Save error %{msg}"
 msgstr "Ошибка сохранения %{msg}"
@@ -1732,7 +1810,7 @@ msgstr "Ошибка сохранения %{msg}"
 msgid "Save successfully"
 msgstr "Успешно сохранено"
 
-#: src/views/config/ConfigEdit.vue:55 src/views/domain/DomainAdd.vue:37
+#: src/views/config/ConfigEditor.vue:150 src/views/domain/DomainAdd.vue:37
 #: src/views/domain/DomainEdit.vue:143
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
 #: src/views/stream/StreamEdit.vue:138
@@ -1763,7 +1841,9 @@ msgstr "Отправлено"
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:42
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:213
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:253
-#: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81
+#: src/views/config/components/Mkdir.vue:38
+#: src/views/config/components/Rename.vue:40
+#: src/views/config/ConfigEditor.vue:93 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
@@ -1817,7 +1897,7 @@ msgstr ""
 msgid "Single Directive"
 msgstr "Одиночная Директива"
 
-#: src/routes/index.ts:207
+#: src/routes/index.ts:218
 #, fuzzy
 msgid "Site Logs"
 msgstr "Логи сайтов"
@@ -1927,7 +2007,7 @@ msgstr "Сертификат действителен"
 msgid "Sync to"
 msgstr ""
 
-#: src/routes/index.ts:258
+#: src/routes/index.ts:269
 msgid "System"
 msgstr "Система"
 
@@ -1940,7 +2020,7 @@ msgstr ""
 msgid "Target"
 msgstr ""
 
-#: src/routes/index.ts:177 src/views/pty/Terminal.vue:114
+#: src/routes/index.ts:188 src/views/pty/Terminal.vue:114
 msgid "Terminal"
 msgstr "Терминал"
 
@@ -2094,18 +2174,19 @@ msgstr ""
 msgid "Trash"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:66
+#: src/components/OTP/useOTPModal.ts:70
 msgid "Two-factor authentication required"
 msgstr ""
 
 #: src/views/certificate/CertificateList/certColumns.tsx:25
-#: src/views/config/config.ts:12 src/views/notification/Notification.vue:13
+#: src/views/config/configColumns.ts:16
+#: src/views/notification/Notification.vue:13
 msgid "Type"
 msgstr "Тип"
 
 #: src/views/certificate/ACMEUser.vue:53
-#: src/views/certificate/DNSCredential.vue:27 src/views/config/config.ts:27
-#: src/views/config/ConfigEdit.vue:121
+#: src/views/certificate/DNSCredential.vue:27
+#: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:258
 #: src/views/domain/components/RightSettings.vue:86
 #: src/views/domain/DomainList.vue:41 src/views/environment/envColumns.tsx:124
 #: src/views/stream/components/RightSettings.vue:85
@@ -2118,7 +2199,7 @@ msgstr "Обновлено в"
 msgid "Updated successfully"
 msgstr "Обновлено успешно"
 
-#: src/routes/index.ts:273 src/views/environment/Environment.vue:50
+#: src/routes/index.ts:284 src/views/environment/Environment.vue:50
 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228
 msgid "Upgrade"
 msgstr "Обновление"
@@ -2251,6 +2332,10 @@ msgstr "Вы используете последнюю версию"
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "Вы можете проверить обновление Nginx UI на этой странице."
 
+#, fuzzy
+#~ msgid "Rename "
+#~ msgstr "Имя пользователя"
+
 #~ msgid "Auto Cert"
 #~ msgstr "Авто Сертификат"
 
@@ -2321,6 +2406,3 @@ msgstr "Вы можете проверить обновление Nginx UI на
 
 #~ msgid "404 Not Found"
 #~ msgstr "404 Not Found"
-
-#~ msgid "Invalid E-mail!"
-#~ msgstr "Invalid E-mail!"

+ 132 - 50
app/src/language/vi_VN/app.po

@@ -17,15 +17,15 @@ msgstr ""
 msgid "2FA Settings"
 msgstr ""
 
-#: src/routes/index.ts:266
+#: src/routes/index.ts:277
 msgid "About"
 msgstr "Tác giả"
 
-#: src/routes/index.ts:193 src/views/domain/ngx_conf/LogEntry.vue:76
+#: src/routes/index.ts:204 src/views/domain/ngx_conf/LogEntry.vue:76
 msgid "Access Logs"
 msgstr "Log truy cập"
 
-#: src/routes/index.ts:131 src/views/certificate/ACMEUser.vue:76
+#: src/routes/index.ts:142 src/views/certificate/ACMEUser.vue:76
 #: src/views/certificate/ACMEUserSelector.vue:85
 #, fuzzy
 msgid "ACME User"
@@ -33,8 +33,9 @@ msgstr "Người dùng"
 
 #: src/views/certificate/ACMEUser.vue:59
 #: src/views/certificate/CertificateList/certColumns.tsx:89
-#: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34
-#: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131
+#: src/views/certificate/DNSCredential.vue:33
+#: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
+#: src/views/environment/envColumns.tsx:131
 #: src/views/notification/Notification.vue:37
 #: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
@@ -51,6 +52,12 @@ msgstr "Hành động"
 msgid "Add"
 msgstr "Thêm"
 
+#: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:128
+#: src/views/config/ConfigEditor.vue:187
+#, fuzzy
+msgid "Add Configuration"
+msgstr "Sửa cấu hình"
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:95
 msgid "Add Directive Below"
 msgstr "Thêm Directive"
@@ -188,9 +195,9 @@ msgid "Auto-renewal enabled for %{name}"
 msgstr "Đã bật tự động gia hạn SSL cho %{name}"
 
 #: src/views/certificate/CertificateEditor.vue:247
-#: src/views/config/Config.vue:71 src/views/config/ConfigEdit.vue:87
-#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168
-#: src/views/stream/StreamEdit.vue:245
+#: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
+#: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
+#: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
 msgid "Back"
 msgstr "Quay lại"
 
@@ -219,7 +226,7 @@ msgstr ""
 msgid "Base information"
 msgstr "Thông tin"
 
-#: src/views/config/ConfigEdit.vue:115
+#: src/views/config/ConfigEditor.vue:224
 #: src/views/domain/components/RightSettings.vue:75
 #: src/views/preference/Preference.vue:110
 #: src/views/stream/components/RightSettings.vue:74
@@ -282,13 +289,13 @@ msgid_plural "Certificates Status"
 msgstr[0] "Trạng thái chứng chỉ"
 msgstr[1] "Trạng thái chứng chỉ"
 
-#: src/routes/index.ts:122
+#: src/routes/index.ts:133
 #: src/views/certificate/CertificateList/Certificate.vue:13
 #, fuzzy
 msgid "Certificates"
 msgstr "Chứng chỉ"
 
-#: src/routes/index.ts:139
+#: src/routes/index.ts:150
 #, fuzzy
 msgid "Certificates List"
 msgstr "Danh sách chứng chỉ"
@@ -310,6 +317,11 @@ msgid_plural "Changed Certificates"
 msgstr[0] "Thay đổi chứng chỉ"
 msgstr[1] "Thay đổi chứng chỉ"
 
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "Changed Path"
+msgstr "Thay đổi chứng chỉ"
+
 #: src/views/environment/BatchUpgrader.vue:161 src/views/system/Upgrade.vue:190
 msgid "Channel"
 msgstr "Kênh"
@@ -359,7 +371,7 @@ msgstr "Tệp cấu hình được kiểm tra thành công"
 msgid "Configuration Name"
 msgstr "Tên cấu hình"
 
-#: src/views/config/Config.vue:42
+#: src/views/config/ConfigList.vue:91
 msgid "Configurations"
 msgstr "Cấu hình"
 
@@ -410,10 +422,25 @@ msgstr "Ngày tạo"
 msgid "Create Another"
 msgstr "Tạo thêm"
 
+#: src/views/config/ConfigList.vue:109
+#, fuzzy
+msgid "Create File"
+msgstr "Ngày tạo"
+
+#: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
+#, fuzzy
+msgid "Create Folder"
+msgstr "Tạo thêm"
+
 #: src/views/notification/Notification.vue:31 src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgstr "Ngày tạo"
 
+#: src/views/config/components/Mkdir.vue:35
+#, fuzzy
+msgid "Created successfully"
+msgstr "Đã xóa thành công"
+
 #: src/language/constants.ts:9
 msgid "Creating client facilitates communication with the CA server"
 msgstr "Tạo client để giao tiếp với CA server"
@@ -449,7 +476,8 @@ msgid ""
 "indicator."
 msgstr ""
 
-#: src/routes/index.ts:39
+#: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
+#: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
 msgid "Dashboard"
 msgstr "Bảng điều khiển"
 
@@ -531,7 +559,7 @@ msgstr "Directive"
 msgid "Directives"
 msgstr "Directives"
 
-#: src/views/config/config.ts:18
+#: src/views/config/configColumns.ts:22
 #, fuzzy
 msgid "Directory"
 msgstr "Thư mục"
@@ -563,7 +591,7 @@ msgstr "Đã tắt thành công"
 msgid "Disk IO"
 msgstr "Disk IO"
 
-#: src/routes/index.ts:167 src/views/certificate/DNSCredential.vue:40
+#: src/routes/index.ts:178 src/views/certificate/DNSCredential.vue:40
 msgid "DNS Credentials"
 msgstr "Xác thực DNS"
 
@@ -688,7 +716,7 @@ msgstr "Đã sao chép thành công vào máy cục bộ"
 msgid "Edit %{n}"
 msgstr "Sửa %{n}"
 
-#: src/routes/index.ts:112 src/views/config/ConfigEdit.vue:81
+#: src/routes/index.ts:122 src/views/config/ConfigEditor.vue:187
 msgid "Edit Configuration"
 msgstr "Sửa cấu hình"
 
@@ -780,7 +808,11 @@ msgstr "Đã bật"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Bảo mật trang web với Let's Encrypt"
 
-#: src/routes/index.ts:217 src/views/environment/Environment.vue:34
+#: src/views/config/ConfigList.vue:151
+msgid "Enter"
+msgstr ""
+
+#: src/routes/index.ts:228 src/views/environment/Environment.vue:34
 msgid "Environment"
 msgstr "Environment"
 
@@ -798,7 +830,7 @@ msgstr "Environments"
 msgid "Error"
 msgstr "Lỗi"
 
-#: src/routes/index.ts:200 src/views/domain/ngx_conf/LogEntry.vue:84
+#: src/routes/index.ts:211 src/views/domain/ngx_conf/LogEntry.vue:84
 msgid "Error Logs"
 msgstr "Log lỗi"
 
@@ -847,7 +879,7 @@ msgstr "Không thể truy xuất thông tin chứng chỉ"
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr "Không lưu được, đã phát hiện thấy (các) lỗi cú pháp trong cấu hình."
 
-#: src/views/config/config.ts:20
+#: src/views/config/configColumns.ts:24
 msgid "File"
 msgstr "Tệp tin"
 
@@ -878,16 +910,16 @@ msgstr "Đã hoàn thành"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr "Người dùng Trung Quốc: https://mirror.ghproxy.com/"
 
-#: src/views/config/ConfigEdit.vue:90
+#: src/views/config/ConfigEditor.vue:199
 msgid "Format Code"
 msgstr "Định dạng code"
 
-#: src/views/config/ConfigEdit.vue:68
+#: src/views/config/ConfigEditor.vue:166
 #, fuzzy
 msgid "Format error %{msg}"
 msgstr "Lưu lỗi %{msg}"
 
-#: src/views/config/ConfigEdit.vue:66
+#: src/views/config/ConfigEditor.vue:164
 #, fuzzy
 msgid "Format successfully"
 msgstr "Định dạng thành công"
@@ -969,7 +1001,7 @@ msgstr ""
 msgid "Import"
 msgstr "Xuất"
 
-#: src/routes/index.ts:157 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:168 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Import Certificate"
 msgstr "Chứng chỉ"
@@ -1000,7 +1032,7 @@ msgstr ""
 msgid "Input the recovery code:"
 msgstr ""
 
-#: src/routes/index.ts:288 src/views/other/Install.vue:134
+#: src/routes/index.ts:299 src/views/other/Install.vue:134
 msgid "Install"
 msgstr "Cài đặt"
 
@@ -1022,7 +1054,17 @@ msgstr "Hợp lệ"
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:60
+#: src/views/config/components/Rename.vue:62
+#: src/views/config/ConfigEditor.vue:233
+#, fuzzy
+msgid "Invalid filename"
+msgstr "E-mail không chính xác!"
+
+#: src/views/config/components/Mkdir.vue:60
+msgid "Invalid folder name"
+msgstr ""
+
+#: src/components/OTP/useOTPModal.ts:64
 msgid "Invalid passcode or recovery code"
 msgstr ""
 
@@ -1128,7 +1170,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "Log"
 
-#: src/routes/index.ts:294 src/views/other/Login.vue:192
+#: src/routes/index.ts:305 src/views/other/Login.vue:192
 msgid "Login"
 msgstr "Đăng nhập"
 
@@ -1163,7 +1205,8 @@ msgstr ""
 "Đảm bảo rằng bạn đã định cấu hình proxy ngược (reverse proxy) thư mục .well-"
 "known tới HTTPChallengePort (default: 9180) trước khi ký chứng chỉ SSL."
 
-#: src/routes/index.ts:102
+#: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
+#: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
 msgid "Manage Configs"
 msgstr "Quản lý cấu hình"
 
@@ -1176,7 +1219,7 @@ msgstr "Quản lý Website"
 msgid "Manage Streams"
 msgstr "Quản lý Website"
 
-#: src/routes/index.ts:240 src/views/user/User.vue:9
+#: src/routes/index.ts:251 src/views/user/User.vue:9
 msgid "Manage Users"
 msgstr "Người dùng"
 
@@ -1209,11 +1252,12 @@ msgstr "Run Mode"
 #: src/components/ChatGPT/ChatGPT.vue:248
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
+#: src/views/config/ConfigList.vue:151
 #, fuzzy
 msgid "Modify"
 msgstr "Sửa"
 
-#: src/routes/index.ts:147 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:158 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "Sửa chứng chỉ"
@@ -1235,7 +1279,9 @@ msgstr "Single Directive"
 #: src/views/certificate/ACMEUser.vue:13
 #: src/views/certificate/CertificateEditor.vue:152
 #: src/views/certificate/CertificateList/certColumns.tsx:10
-#: src/views/certificate/DNSCredential.vue:11 src/views/config/config.ts:7
+#: src/views/certificate/DNSCredential.vue:11
+#: src/views/config/components/Mkdir.vue:67 src/views/config/configColumns.ts:8
+#: src/views/config/ConfigEditor.vue:239
 #: src/views/domain/components/RightSettings.vue:83
 #: src/views/domain/components/SiteDuplicate.vue:129
 #: src/views/domain/DomainList.vue:13
@@ -1263,6 +1309,16 @@ msgstr "Tổng lưu lượng mạng đã nhận"
 msgid "Network Total Send"
 msgstr "Tổng lưu lượng mạng đã gửi"
 
+#: src/views/config/components/Rename.vue:70
+#, fuzzy
+msgid "New name"
+msgstr "Username"
+
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "New Path"
+msgstr "Đường dẫn"
+
 #: src/views/system/Upgrade.vue:210
 msgid "New version released"
 msgstr "Đã có phiên bản mới"
@@ -1294,7 +1350,7 @@ msgstr ""
 msgid "Nginx Error Log Path"
 msgstr "Vị trí lưu log lỗi (Error log) của Nginx"
 
-#: src/routes/index.ts:185 src/views/nginx_log/NginxLog.vue:143
+#: src/routes/index.ts:196 src/views/nginx_log/NginxLog.vue:143
 msgid "Nginx Log"
 msgstr ""
 
@@ -1331,7 +1387,7 @@ msgstr ""
 msgid "Not After"
 msgstr "Không phải sau khi"
 
-#: src/routes/index.ts:300
+#: src/routes/index.ts:311
 msgid "Not Found"
 msgstr "Không tìm thấy"
 
@@ -1349,7 +1405,7 @@ msgstr "Ghi chú"
 msgid "Notification"
 msgstr "Thông báo"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:231
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:242
 #, fuzzy
 msgid "Notifications"
 msgstr "Thông báo"
@@ -1419,6 +1475,10 @@ msgstr "Trực tuyến"
 msgid "OpenAI"
 msgstr ""
 
+#: src/views/config/components/Rename.vue:66
+msgid "Original name"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:177
 #, fuzzy
 msgid "OS"
@@ -1450,7 +1510,7 @@ msgstr "Mật khẩu"
 msgid "Password (*)"
 msgstr "Mật khẩu (*)"
 
-#: src/views/config/ConfigEdit.vue:118
+#: src/views/config/ConfigEditor.vue:245
 #: src/views/domain/ngx_conf/LocationEditor.vue:118
 #: src/views/domain/ngx_conf/LocationEditor.vue:90
 msgid "Path"
@@ -1491,6 +1551,17 @@ msgstr ""
 "Trước tiên, vui lòng thêm thông tin xác thực trong Chứng chỉ > Thông tin xác "
 "thực DNS, sau đó chọn nhà cung cấp DNS"
 
+#: src/views/config/components/Rename.vue:61
+#: src/views/config/ConfigEditor.vue:232
+#, fuzzy
+msgid "Please input a filename"
+msgstr "Vui lòng nhập username!"
+
+#: src/views/config/components/Mkdir.vue:59
+#, fuzzy
+msgid "Please input a folder name"
+msgstr "Vui lòng nhập username!"
+
 #: src/views/domain/components/SiteDuplicate.vue:38
 #: src/views/stream/components/StreamDuplicate.vue:38
 msgid ""
@@ -1527,7 +1598,7 @@ msgstr ""
 msgid "Pre-release"
 msgstr ""
 
-#: src/routes/index.ts:249 src/views/preference/Preference.vue:105
+#: src/routes/index.ts:260 src/views/preference/Preference.vue:105
 msgid "Preference"
 msgstr "Cài đặt"
 
@@ -1652,11 +1723,18 @@ msgstr "Xoá thành công"
 msgid "Removed successfully"
 msgstr "Xoá thành công"
 
+#: src/views/config/components/Rename.vue:52
+#: src/views/config/ConfigList.vue:159
 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
 #, fuzzy
 msgid "Rename"
 msgstr "Username"
 
+#: src/views/config/components/Rename.vue:37
+#, fuzzy
+msgid "Rename successfully"
+msgstr "Gia hạn chứng chỉ SSL"
+
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1711,7 +1789,7 @@ msgstr "Running"
 
 #: src/components/ChatGPT/ChatGPT.vue:251
 #: src/views/certificate/CertificateEditor.vue:254
-#: src/views/config/ConfigEdit.vue:96 src/views/domain/DomainEdit.vue:260
+#: src/views/config/ConfigEditor.vue:205 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/preference/Preference.vue:145 src/views/stream/StreamEdit.vue:252
 msgid "Save"
@@ -1721,7 +1799,7 @@ msgstr "Lưu"
 msgid "Save Directive"
 msgstr "Lưu Directive"
 
-#: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:46
+#: src/views/config/ConfigEditor.vue:154 src/views/domain/DomainAdd.vue:46
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
 msgid "Save error %{msg}"
 msgstr "Đã xảy ra lỗi khi lưu %{msg}"
@@ -1734,7 +1812,7 @@ msgstr "Đã xảy ra lỗi khi lưu %{msg}"
 msgid "Save successfully"
 msgstr "Lưu thành công"
 
-#: src/views/config/ConfigEdit.vue:55 src/views/domain/DomainAdd.vue:37
+#: src/views/config/ConfigEditor.vue:150 src/views/domain/DomainAdd.vue:37
 #: src/views/domain/DomainEdit.vue:143
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
 #: src/views/stream/StreamEdit.vue:138
@@ -1765,7 +1843,9 @@ msgstr "Gửi"
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:42
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:213
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:253
-#: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81
+#: src/views/config/components/Mkdir.vue:38
+#: src/views/config/components/Rename.vue:40
+#: src/views/config/ConfigEditor.vue:93 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
@@ -1820,7 +1900,7 @@ msgstr ""
 msgid "Single Directive"
 msgstr "Single Directive"
 
-#: src/routes/index.ts:207
+#: src/routes/index.ts:218
 #, fuzzy
 msgid "Site Logs"
 msgstr "Logs"
@@ -1925,7 +2005,7 @@ msgstr "Gia hạn chứng chỉ SSL thành công"
 msgid "Sync to"
 msgstr ""
 
-#: src/routes/index.ts:258
+#: src/routes/index.ts:269
 msgid "System"
 msgstr "Thông tin"
 
@@ -1938,7 +2018,7 @@ msgstr ""
 msgid "Target"
 msgstr "Mục tiêu"
 
-#: src/routes/index.ts:177 src/views/pty/Terminal.vue:114
+#: src/routes/index.ts:188 src/views/pty/Terminal.vue:114
 msgid "Terminal"
 msgstr "Terminal"
 
@@ -2090,18 +2170,19 @@ msgstr ""
 msgid "Trash"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:66
+#: src/components/OTP/useOTPModal.ts:70
 msgid "Two-factor authentication required"
 msgstr ""
 
 #: src/views/certificate/CertificateList/certColumns.tsx:25
-#: src/views/config/config.ts:12 src/views/notification/Notification.vue:13
+#: src/views/config/configColumns.ts:16
+#: src/views/notification/Notification.vue:13
 msgid "Type"
 msgstr "Loại"
 
 #: src/views/certificate/ACMEUser.vue:53
-#: src/views/certificate/DNSCredential.vue:27 src/views/config/config.ts:27
-#: src/views/config/ConfigEdit.vue:121
+#: src/views/certificate/DNSCredential.vue:27
+#: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:258
 #: src/views/domain/components/RightSettings.vue:86
 #: src/views/domain/DomainList.vue:41 src/views/environment/envColumns.tsx:124
 #: src/views/stream/components/RightSettings.vue:85
@@ -2114,7 +2195,7 @@ msgstr "Ngày cập nhật"
 msgid "Updated successfully"
 msgstr "Cập nhật thành công"
 
-#: src/routes/index.ts:273 src/views/environment/Environment.vue:50
+#: src/routes/index.ts:284 src/views/environment/Environment.vue:50
 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228
 msgid "Upgrade"
 msgstr "Cập nhật"
@@ -2250,6 +2331,10 @@ msgstr "Bạn đang sử dụng phiên bản mới nhất"
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "Bạn có thể kiểm tra nâng cấp Nginx UI tại trang này"
 
+#, fuzzy
+#~ msgid "Rename "
+#~ msgstr "Username"
+
 #~ msgid "Auto Cert"
 #~ msgstr "Tự động ký chứng chỉ SSL"
 
@@ -2291,6 +2376,3 @@ msgstr "Bạn có thể kiểm tra nâng cấp Nginx UI tại trang này"
 
 #~ msgid "404 Not Found"
 #~ msgstr "404 Not Found"
-
-#~ msgid "Invalid E-mail!"
-#~ msgstr "E-mail không chính xác!"

BIN
app/src/language/zh_CN/app.mo


+ 121 - 50
app/src/language/zh_CN/app.po

@@ -21,23 +21,24 @@ msgstr "2FA"
 msgid "2FA Settings"
 msgstr "2FA 设置"
 
-#: src/routes/index.ts:266
+#: src/routes/index.ts:277
 msgid "About"
 msgstr "关于"
 
-#: src/routes/index.ts:193 src/views/domain/ngx_conf/LogEntry.vue:76
+#: src/routes/index.ts:204 src/views/domain/ngx_conf/LogEntry.vue:76
 msgid "Access Logs"
 msgstr "访问日志"
 
-#: src/routes/index.ts:131 src/views/certificate/ACMEUser.vue:76
+#: src/routes/index.ts:142 src/views/certificate/ACMEUser.vue:76
 #: src/views/certificate/ACMEUserSelector.vue:85
 msgid "ACME User"
 msgstr "ACME 用户"
 
 #: src/views/certificate/ACMEUser.vue:59
 #: src/views/certificate/CertificateList/certColumns.tsx:89
-#: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34
-#: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131
+#: src/views/certificate/DNSCredential.vue:33
+#: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
+#: src/views/environment/envColumns.tsx:131
 #: src/views/notification/Notification.vue:37
 #: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
@@ -54,6 +55,11 @@ msgstr "操作"
 msgid "Add"
 msgstr "添加"
 
+#: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:128
+#: src/views/config/ConfigEditor.vue:187
+msgid "Add Configuration"
+msgstr "添加配置"
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:95
 msgid "Add Directive Below"
 msgstr "在下面添加指令"
@@ -178,9 +184,9 @@ msgid "Auto-renewal enabled for %{name}"
 msgstr "成功启用 %{name} 自动续签"
 
 #: src/views/certificate/CertificateEditor.vue:247
-#: src/views/config/Config.vue:71 src/views/config/ConfigEdit.vue:87
-#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168
-#: src/views/stream/StreamEdit.vue:245
+#: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
+#: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
+#: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
 msgid "Back"
 msgstr "返回"
 
@@ -208,7 +214,7 @@ msgstr "禁用至"
 msgid "Base information"
 msgstr "基本信息"
 
-#: src/views/config/ConfigEdit.vue:115
+#: src/views/config/ConfigEditor.vue:224
 #: src/views/domain/components/RightSettings.vue:75
 #: src/views/preference/Preference.vue:110
 #: src/views/stream/components/RightSettings.vue:74
@@ -265,12 +271,12 @@ msgid "Certificate Status"
 msgid_plural "Certificates Status"
 msgstr[0] "证书状态"
 
-#: src/routes/index.ts:122
+#: src/routes/index.ts:133
 #: src/views/certificate/CertificateList/Certificate.vue:13
 msgid "Certificates"
 msgstr "证书"
 
-#: src/routes/index.ts:139
+#: src/routes/index.ts:150
 msgid "Certificates List"
 msgstr "证书列表"
 
@@ -288,6 +294,10 @@ msgid "Changed Certificate"
 msgid_plural "Changed Certificates"
 msgstr[0] "变更证书"
 
+#: src/views/config/ConfigEditor.vue:251
+msgid "Changed Path"
+msgstr "变更后的路径"
+
 #: src/views/environment/BatchUpgrader.vue:161 src/views/system/Upgrade.vue:190
 msgid "Channel"
 msgstr "通道"
@@ -334,7 +344,7 @@ msgstr "配置文件测试成功"
 msgid "Configuration Name"
 msgstr "配置名称"
 
-#: src/views/config/Config.vue:42
+#: src/views/config/ConfigList.vue:91
 msgid "Configurations"
 msgstr "配置"
 
@@ -384,10 +394,22 @@ msgstr "创建"
 msgid "Create Another"
 msgstr "再创建一个"
 
+#: src/views/config/ConfigList.vue:109
+msgid "Create File"
+msgstr "创建文件"
+
+#: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
+msgid "Create Folder"
+msgstr "创建文件夹"
+
 #: src/views/notification/Notification.vue:31 src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgstr "创建时间"
 
+#: src/views/config/components/Mkdir.vue:35
+msgid "Created successfully"
+msgstr "创建成功"
+
 #: src/language/constants.ts:9
 msgid "Creating client facilitates communication with the CA server"
 msgstr "正在创建客户端用于与 CA 服务器通信"
@@ -423,7 +445,8 @@ msgid ""
 "indicator."
 msgstr "自定义显示在环境指示器中的本地服务器名称。"
 
-#: src/routes/index.ts:39
+#: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
+#: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
 msgid "Dashboard"
 msgstr "仪表盘"
 
@@ -502,7 +525,7 @@ msgstr "指令"
 msgid "Directives"
 msgstr "指令"
 
-#: src/views/config/config.ts:18
+#: src/views/config/configColumns.ts:22
 msgid "Directory"
 msgstr "目录"
 
@@ -532,7 +555,7 @@ msgstr "禁用成功"
 msgid "Disk IO"
 msgstr "磁盘 IO"
 
-#: src/routes/index.ts:167 src/views/certificate/DNSCredential.vue:40
+#: src/routes/index.ts:178 src/views/certificate/DNSCredential.vue:40
 msgid "DNS Credentials"
 msgstr "DNS 凭证"
 
@@ -642,7 +665,7 @@ msgstr "成功复制到本地"
 msgid "Edit %{n}"
 msgstr "编辑 %{n}"
 
-#: src/routes/index.ts:112 src/views/config/ConfigEdit.vue:81
+#: src/routes/index.ts:122 src/views/config/ConfigEditor.vue:187
 msgid "Edit Configuration"
 msgstr "编辑配置"
 
@@ -728,7 +751,11 @@ msgstr "启用成功"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "用 Let's Encrypt 对网站进行加密"
 
-#: src/routes/index.ts:217 src/views/environment/Environment.vue:34
+#: src/views/config/ConfigList.vue:151
+msgid "Enter"
+msgstr "进入"
+
+#: src/routes/index.ts:228 src/views/environment/Environment.vue:34
 msgid "Environment"
 msgstr "环境"
 
@@ -744,7 +771,7 @@ msgstr "环境"
 msgid "Error"
 msgstr "错误"
 
-#: src/routes/index.ts:200 src/views/domain/ngx_conf/LogEntry.vue:84
+#: src/routes/index.ts:211 src/views/domain/ngx_conf/LogEntry.vue:84
 msgid "Error Logs"
 msgstr "错误日志"
 
@@ -791,7 +818,7 @@ msgstr "获取证书信息失败"
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr "保存失败,在配置中检测到语法错误。"
 
-#: src/views/config/config.ts:20
+#: src/views/config/configColumns.ts:24
 msgid "File"
 msgstr "文件"
 
@@ -820,15 +847,15 @@ msgstr "完成"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr "中国用户:https://mirror.ghproxy.com/"
 
-#: src/views/config/ConfigEdit.vue:90
+#: src/views/config/ConfigEditor.vue:199
 msgid "Format Code"
 msgstr "代码格式化"
 
-#: src/views/config/ConfigEdit.vue:68
+#: src/views/config/ConfigEditor.vue:166
 msgid "Format error %{msg}"
 msgstr "保存错误 %{msg}"
 
-#: src/views/config/ConfigEdit.vue:66
+#: src/views/config/ConfigEditor.vue:164
 msgid "Format successfully"
 msgstr "格式化成功"
 
@@ -908,7 +935,7 @@ msgstr "如果您的域名有 CNAME 记录且无法获取证书,则需要启
 msgid "Import"
 msgstr "导入"
 
-#: src/routes/index.ts:157 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:168 src/views/certificate/CertificateEditor.vue:79
 msgid "Import Certificate"
 msgstr "导入证书"
 
@@ -937,7 +964,7 @@ msgstr "输入应用程序中的代码:"
 msgid "Input the recovery code:"
 msgstr "输入恢复代码:"
 
-#: src/routes/index.ts:288 src/views/other/Install.vue:134
+#: src/routes/index.ts:299 src/views/other/Install.vue:134
 msgid "Install"
 msgstr "安装"
 
@@ -957,7 +984,16 @@ msgstr "无效的"
 msgid "Invalid 2FA or recovery code"
 msgstr "无效的二步验证码或恢复密码"
 
-#: src/components/OTP/useOTPModal.ts:60
+#: src/views/config/components/Rename.vue:62
+#: src/views/config/ConfigEditor.vue:233
+msgid "Invalid filename"
+msgstr "文件名无效"
+
+#: src/views/config/components/Mkdir.vue:60
+msgid "Invalid folder name"
+msgstr "无效文件夹名"
+
+#: src/components/OTP/useOTPModal.ts:64
 msgid "Invalid passcode or recovery code"
 msgstr "二次验证码或恢复代码无效"
 
@@ -1053,7 +1089,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "日志"
 
-#: src/routes/index.ts:294 src/views/other/Login.vue:192
+#: src/routes/index.ts:305 src/views/other/Login.vue:192
 msgid "Login"
 msgstr "登录"
 
@@ -1091,7 +1127,8 @@ msgstr ""
 "在获取签发证书前,请确保配置文件中已将 .well-known 目录反向代理到 "
 "HTTPChallengePort。"
 
-#: src/routes/index.ts:102
+#: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
+#: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
 msgid "Manage Configs"
 msgstr "配置管理"
 
@@ -1103,7 +1140,7 @@ msgstr "网站管理"
 msgid "Manage Streams"
 msgstr "管理 Stream"
 
-#: src/routes/index.ts:240 src/views/user/User.vue:9
+#: src/routes/index.ts:251 src/views/user/User.vue:9
 msgid "Manage Users"
 msgstr "用户管理"
 
@@ -1135,10 +1172,11 @@ msgstr "模型"
 #: src/components/ChatGPT/ChatGPT.vue:248
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
+#: src/views/config/ConfigList.vue:151
 msgid "Modify"
 msgstr "修改"
 
-#: src/routes/index.ts:147 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:158 src/views/certificate/CertificateEditor.vue:79
 msgid "Modify Certificate"
 msgstr "修改证书"
 
@@ -1157,7 +1195,9 @@ msgstr "多行指令"
 #: src/views/certificate/ACMEUser.vue:13
 #: src/views/certificate/CertificateEditor.vue:152
 #: src/views/certificate/CertificateList/certColumns.tsx:10
-#: src/views/certificate/DNSCredential.vue:11 src/views/config/config.ts:7
+#: src/views/certificate/DNSCredential.vue:11
+#: src/views/config/components/Mkdir.vue:67 src/views/config/configColumns.ts:8
+#: src/views/config/ConfigEditor.vue:239
 #: src/views/domain/components/RightSettings.vue:83
 #: src/views/domain/components/SiteDuplicate.vue:129
 #: src/views/domain/DomainList.vue:13
@@ -1185,6 +1225,14 @@ msgstr "下载流量"
 msgid "Network Total Send"
 msgstr "上传流量"
 
+#: src/views/config/components/Rename.vue:70
+msgid "New name"
+msgstr "新名称"
+
+#: src/views/config/ConfigEditor.vue:251
+msgid "New Path"
+msgstr "新路径"
+
 #: src/views/system/Upgrade.vue:210
 msgid "New version released"
 msgstr "新版本发布"
@@ -1215,7 +1263,7 @@ msgstr "控制 Nginx"
 msgid "Nginx Error Log Path"
 msgstr "Nginx 错误日志路径"
 
-#: src/routes/index.ts:185 src/views/nginx_log/NginxLog.vue:143
+#: src/routes/index.ts:196 src/views/nginx_log/NginxLog.vue:143
 msgid "Nginx Log"
 msgstr "Nginx 日志"
 
@@ -1250,7 +1298,7 @@ msgstr "节点密钥"
 msgid "Not After"
 msgstr "有效期"
 
-#: src/routes/index.ts:300
+#: src/routes/index.ts:311
 msgid "Not Found"
 msgstr "找不到页面"
 
@@ -1267,7 +1315,7 @@ msgstr "注意"
 msgid "Notification"
 msgstr "通知"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:231
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:242
 msgid "Notifications"
 msgstr "通知"
 
@@ -1335,6 +1383,10 @@ msgstr "在线"
 msgid "OpenAI"
 msgstr "OpenAI"
 
+#: src/views/config/components/Rename.vue:66
+msgid "Original name"
+msgstr "原名"
+
 #: src/views/system/Upgrade.vue:177
 msgid "OS"
 msgstr "OS"
@@ -1365,7 +1417,7 @@ msgstr "密码"
 msgid "Password (*)"
 msgstr "密码 (*)"
 
-#: src/views/config/ConfigEdit.vue:118
+#: src/views/config/ConfigEditor.vue:245
 #: src/views/domain/ngx_conf/LocationEditor.vue:118
 #: src/views/domain/ngx_conf/LocationEditor.vue:90
 msgid "Path"
@@ -1405,6 +1457,15 @@ msgstr ""
 "请首先在 “证书”> “DNS 凭证” 中添加凭证,然后在下方选择一个凭证,请求 DNS 提供"
 "商的 API。"
 
+#: src/views/config/components/Rename.vue:61
+#: src/views/config/ConfigEditor.vue:232
+msgid "Please input a filename"
+msgstr "请输入文件名"
+
+#: src/views/config/components/Mkdir.vue:59
+msgid "Please input a folder name"
+msgstr "请输入文件夹名称"
+
 #: src/views/domain/components/SiteDuplicate.vue:38
 #: src/views/stream/components/StreamDuplicate.vue:38
 msgid ""
@@ -1440,7 +1501,7 @@ msgstr "请至少选择一个节点!"
 msgid "Pre-release"
 msgstr "预发布"
 
-#: src/routes/index.ts:249 src/views/preference/Preference.vue:105
+#: src/routes/index.ts:260 src/views/preference/Preference.vue:105
 msgid "Preference"
 msgstr "偏好设置"
 
@@ -1556,10 +1617,16 @@ msgstr "移除成功"
 msgid "Removed successfully"
 msgstr "删除成功"
 
+#: src/views/config/components/Rename.vue:52
+#: src/views/config/ConfigList.vue:159
 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
 msgid "Rename"
 msgstr "重命名"
 
+#: src/views/config/components/Rename.vue:37
+msgid "Rename successfully"
+msgstr "重命名成功"
+
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 msgid "Renew Certificate"
@@ -1608,7 +1675,7 @@ msgstr "运行中"
 
 #: src/components/ChatGPT/ChatGPT.vue:251
 #: src/views/certificate/CertificateEditor.vue:254
-#: src/views/config/ConfigEdit.vue:96 src/views/domain/DomainEdit.vue:260
+#: src/views/config/ConfigEditor.vue:205 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/preference/Preference.vue:145 src/views/stream/StreamEdit.vue:252
 msgid "Save"
@@ -1618,7 +1685,7 @@ msgstr "保存"
 msgid "Save Directive"
 msgstr "保存指令"
 
-#: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:46
+#: src/views/config/ConfigEditor.vue:154 src/views/domain/DomainAdd.vue:46
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
 msgid "Save error %{msg}"
 msgstr "保存错误 %{msg}"
@@ -1630,7 +1697,7 @@ msgstr "保存错误 %{msg}"
 msgid "Save successfully"
 msgstr "保存成功"
 
-#: src/views/config/ConfigEdit.vue:55 src/views/domain/DomainAdd.vue:37
+#: src/views/config/ConfigEditor.vue:150 src/views/domain/DomainAdd.vue:37
 #: src/views/domain/DomainEdit.vue:143
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
 #: src/views/stream/StreamEdit.vue:138
@@ -1661,7 +1728,9 @@ msgstr "上传"
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:42
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:213
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:253
-#: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81
+#: src/views/config/components/Mkdir.vue:38
+#: src/views/config/components/Rename.vue:40
+#: src/views/config/ConfigEditor.vue:93 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
@@ -1714,7 +1783,7 @@ msgstr "显示"
 msgid "Single Directive"
 msgstr "单行指令"
 
-#: src/routes/index.ts:207
+#: src/routes/index.ts:218
 msgid "Site Logs"
 msgstr "站点列表"
 
@@ -1813,7 +1882,7 @@ msgstr "同步证书成功"
 msgid "Sync to"
 msgstr "同步到"
 
-#: src/routes/index.ts:258
+#: src/routes/index.ts:269
 msgid "System"
 msgstr "系统"
 
@@ -1826,7 +1895,7 @@ msgstr "系统初始用户"
 msgid "Target"
 msgstr "目标"
 
-#: src/routes/index.ts:177 src/views/pty/Terminal.vue:114
+#: src/routes/index.ts:188 src/views/pty/Terminal.vue:114
 msgid "Terminal"
 msgstr "终端"
 
@@ -1976,18 +2045,19 @@ msgstr "TOTP 是一种使用基于时间的一次性密码算法的双因素身
 msgid "Trash"
 msgstr "回收站"
 
-#: src/components/OTP/useOTPModal.ts:66
+#: src/components/OTP/useOTPModal.ts:70
 msgid "Two-factor authentication required"
 msgstr "需要两步验证"
 
 #: src/views/certificate/CertificateList/certColumns.tsx:25
-#: src/views/config/config.ts:12 src/views/notification/Notification.vue:13
+#: src/views/config/configColumns.ts:16
+#: src/views/notification/Notification.vue:13
 msgid "Type"
 msgstr "类型"
 
 #: src/views/certificate/ACMEUser.vue:53
-#: src/views/certificate/DNSCredential.vue:27 src/views/config/config.ts:27
-#: src/views/config/ConfigEdit.vue:121
+#: src/views/certificate/DNSCredential.vue:27
+#: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:258
 #: src/views/domain/components/RightSettings.vue:86
 #: src/views/domain/DomainList.vue:41 src/views/environment/envColumns.tsx:124
 #: src/views/stream/components/RightSettings.vue:85
@@ -1999,7 +2069,7 @@ msgstr "修改时间"
 msgid "Updated successfully"
 msgstr "更新成功"
 
-#: src/routes/index.ts:273 src/views/environment/Environment.vue:50
+#: src/routes/index.ts:284 src/views/environment/Environment.vue:50
 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228
 msgid "Upgrade"
 msgstr "升级"
@@ -2125,6 +2195,10 @@ msgstr "您使用的是最新版本"
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "你可以在这个页面检查Nginx UI的升级。"
 
+#, fuzzy
+#~ msgid "Rename "
+#~ msgstr "重命名"
+
 #~ msgid "Auto Cert"
 #~ msgstr "自动更新"
 
@@ -2277,9 +2351,6 @@ msgstr "你可以在这个页面检查Nginx UI的升级。"
 #~ msgid "404 Not Found"
 #~ msgstr "404 未找到页面"
 
-#~ msgid "Invalid E-mail!"
-#~ msgstr "无效的邮箱!"
-
 #~ msgid "Are you sure you want to restore?"
 #~ msgstr "您确定要反删除?"
 

+ 132 - 47
app/src/language/zh_TW/app.po

@@ -22,15 +22,15 @@ msgstr ""
 msgid "2FA Settings"
 msgstr ""
 
-#: src/routes/index.ts:266
+#: src/routes/index.ts:277
 msgid "About"
 msgstr "關於"
 
-#: src/routes/index.ts:193 src/views/domain/ngx_conf/LogEntry.vue:76
+#: src/routes/index.ts:204 src/views/domain/ngx_conf/LogEntry.vue:76
 msgid "Access Logs"
 msgstr "存取日誌"
 
-#: src/routes/index.ts:131 src/views/certificate/ACMEUser.vue:76
+#: src/routes/index.ts:142 src/views/certificate/ACMEUser.vue:76
 #: src/views/certificate/ACMEUserSelector.vue:85
 #, fuzzy
 msgid "ACME User"
@@ -38,8 +38,9 @@ msgstr "使用者名稱"
 
 #: src/views/certificate/ACMEUser.vue:59
 #: src/views/certificate/CertificateList/certColumns.tsx:89
-#: src/views/certificate/DNSCredential.vue:33 src/views/config/config.ts:34
-#: src/views/domain/DomainList.vue:47 src/views/environment/envColumns.tsx:131
+#: src/views/certificate/DNSCredential.vue:33
+#: src/views/config/configColumns.ts:38 src/views/domain/DomainList.vue:47
+#: src/views/environment/envColumns.tsx:131
 #: src/views/notification/Notification.vue:37
 #: src/views/preference/AuthSettings.vue:26 src/views/stream/StreamList.vue:47
 #: src/views/user/userColumns.tsx:60
@@ -56,6 +57,12 @@ msgstr "操作"
 msgid "Add"
 msgstr "新增"
 
+#: src/routes/index.ts:112 src/views/config/ConfigEditor.vue:128
+#: src/views/config/ConfigEditor.vue:187
+#, fuzzy
+msgid "Add Configuration"
+msgstr "編輯設定"
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:95
 msgid "Add Directive Below"
 msgstr "在下方新增指令"
@@ -190,9 +197,9 @@ msgid "Auto-renewal enabled for %{name}"
 msgstr "已啟用 %{name} 的自動續簽"
 
 #: src/views/certificate/CertificateEditor.vue:247
-#: src/views/config/Config.vue:71 src/views/config/ConfigEdit.vue:87
-#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168
-#: src/views/stream/StreamEdit.vue:245
+#: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
+#: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
+#: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
 msgid "Back"
 msgstr "返回"
 
@@ -220,7 +227,7 @@ msgstr ""
 msgid "Base information"
 msgstr "基本資訊"
 
-#: src/views/config/ConfigEdit.vue:115
+#: src/views/config/ConfigEditor.vue:224
 #: src/views/domain/components/RightSettings.vue:75
 #: src/views/preference/Preference.vue:110
 #: src/views/stream/components/RightSettings.vue:74
@@ -280,13 +287,13 @@ msgid "Certificate Status"
 msgid_plural "Certificates Status"
 msgstr[0] "憑證狀態"
 
-#: src/routes/index.ts:122
+#: src/routes/index.ts:133
 #: src/views/certificate/CertificateList/Certificate.vue:13
 #, fuzzy
 msgid "Certificates"
 msgstr "憑證狀態"
 
-#: src/routes/index.ts:139
+#: src/routes/index.ts:150
 #, fuzzy
 msgid "Certificates List"
 msgstr "憑證清單"
@@ -306,6 +313,11 @@ msgid "Changed Certificate"
 msgid_plural "Changed Certificates"
 msgstr[0] "更換憑證"
 
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "Changed Path"
+msgstr "更換憑證"
+
 #: src/views/environment/BatchUpgrader.vue:161 src/views/system/Upgrade.vue:190
 msgid "Channel"
 msgstr "通道"
@@ -354,7 +366,7 @@ msgstr "設定檔案測試成功"
 msgid "Configuration Name"
 msgstr "設定名稱"
 
-#: src/views/config/Config.vue:42
+#: src/views/config/ConfigList.vue:91
 msgid "Configurations"
 msgstr "設定"
 
@@ -405,10 +417,25 @@ msgstr "建立時間"
 msgid "Create Another"
 msgstr "再建立一個"
 
+#: src/views/config/ConfigList.vue:109
+#, fuzzy
+msgid "Create File"
+msgstr "建立時間"
+
+#: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
+#, fuzzy
+msgid "Create Folder"
+msgstr "再建立一個"
+
 #: src/views/notification/Notification.vue:31 src/views/user/userColumns.tsx:48
 msgid "Created at"
 msgstr "建立時間"
 
+#: src/views/config/components/Mkdir.vue:35
+#, fuzzy
+msgid "Created successfully"
+msgstr "成功停用"
+
 #: src/language/constants.ts:9
 msgid "Creating client facilitates communication with the CA server"
 msgstr "建立客戶端方便與CA伺服器通訊"
@@ -444,7 +471,8 @@ msgid ""
 "indicator."
 msgstr ""
 
-#: src/routes/index.ts:39
+#: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
+#: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
 msgid "Dashboard"
 msgstr "儀表板"
 
@@ -525,7 +553,7 @@ msgstr "指令"
 msgid "Directives"
 msgstr "指令"
 
-#: src/views/config/config.ts:18
+#: src/views/config/configColumns.ts:22
 #, fuzzy
 msgid "Directory"
 msgstr "指令"
@@ -556,7 +584,7 @@ msgstr "成功停用"
 msgid "Disk IO"
 msgstr "磁碟 IO"
 
-#: src/routes/index.ts:167 src/views/certificate/DNSCredential.vue:40
+#: src/routes/index.ts:178 src/views/certificate/DNSCredential.vue:40
 msgid "DNS Credentials"
 msgstr "DNS 認證"
 
@@ -670,7 +698,7 @@ msgstr "成功複製至本機"
 msgid "Edit %{n}"
 msgstr "編輯 %{n}"
 
-#: src/routes/index.ts:112 src/views/config/ConfigEdit.vue:81
+#: src/routes/index.ts:122 src/views/config/ConfigEditor.vue:187
 msgid "Edit Configuration"
 msgstr "編輯設定"
 
@@ -760,7 +788,11 @@ msgstr "成功啟用"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "用 Let's Encrypt 對網站進行加密"
 
-#: src/routes/index.ts:217 src/views/environment/Environment.vue:34
+#: src/views/config/ConfigList.vue:151
+msgid "Enter"
+msgstr ""
+
+#: src/routes/index.ts:228 src/views/environment/Environment.vue:34
 msgid "Environment"
 msgstr "環境"
 
@@ -777,7 +809,7 @@ msgstr "環境"
 msgid "Error"
 msgstr "錯誤"
 
-#: src/routes/index.ts:200 src/views/domain/ngx_conf/LogEntry.vue:84
+#: src/routes/index.ts:211 src/views/domain/ngx_conf/LogEntry.vue:84
 msgid "Error Logs"
 msgstr "錯誤日誌"
 
@@ -826,7 +858,7 @@ msgstr "取得憑證資訊失敗"
 msgid "Failed to save, syntax error(s) was detected in the configuration."
 msgstr "儲存失敗,在設定中檢測到語法錯誤。"
 
-#: src/views/config/config.ts:20
+#: src/views/config/configColumns.ts:24
 msgid "File"
 msgstr "檔案"
 
@@ -857,15 +889,15 @@ msgstr "完成"
 msgid "For Chinese user: https://mirror.ghproxy.com/"
 msgstr "中國使用者:https://mirror.ghproxy.com/"
 
-#: src/views/config/ConfigEdit.vue:90
+#: src/views/config/ConfigEditor.vue:199
 msgid "Format Code"
 msgstr "格式化程式碼"
 
-#: src/views/config/ConfigEdit.vue:68
+#: src/views/config/ConfigEditor.vue:166
 msgid "Format error %{msg}"
 msgstr "格式錯誤 %{msg}"
 
-#: src/views/config/ConfigEdit.vue:66
+#: src/views/config/ConfigEditor.vue:164
 msgid "Format successfully"
 msgstr "成功格式化"
 
@@ -945,7 +977,7 @@ msgstr ""
 msgid "Import"
 msgstr "匯出"
 
-#: src/routes/index.ts:157 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:168 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Import Certificate"
 msgstr "憑證狀態"
@@ -976,7 +1008,7 @@ msgstr ""
 msgid "Input the recovery code:"
 msgstr ""
 
-#: src/routes/index.ts:288 src/views/other/Install.vue:134
+#: src/routes/index.ts:299 src/views/other/Install.vue:134
 msgid "Install"
 msgstr "安裝"
 
@@ -997,7 +1029,17 @@ msgstr "無效的郵箱!"
 msgid "Invalid 2FA or recovery code"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:60
+#: src/views/config/components/Rename.vue:62
+#: src/views/config/ConfigEditor.vue:233
+#, fuzzy
+msgid "Invalid filename"
+msgstr "無效的郵箱!"
+
+#: src/views/config/components/Mkdir.vue:60
+msgid "Invalid folder name"
+msgstr ""
+
+#: src/components/OTP/useOTPModal.ts:64
 msgid "Invalid passcode or recovery code"
 msgstr ""
 
@@ -1101,7 +1143,7 @@ msgstr "Locations"
 msgid "Log"
 msgstr "登入"
 
-#: src/routes/index.ts:294 src/views/other/Login.vue:192
+#: src/routes/index.ts:305 src/views/other/Login.vue:192
 msgid "Login"
 msgstr "登入"
 
@@ -1134,7 +1176,8 @@ msgid ""
 msgstr ""
 "在取得憑證前,請確保您已將 .well-known 目錄反向代理到 HTTPChallengePort。"
 
-#: src/routes/index.ts:102
+#: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
+#: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
 msgid "Manage Configs"
 msgstr "管理設定"
 
@@ -1147,7 +1190,7 @@ msgstr "管理網站"
 msgid "Manage Streams"
 msgstr "管理網站"
 
-#: src/routes/index.ts:240 src/views/user/User.vue:9
+#: src/routes/index.ts:251 src/views/user/User.vue:9
 msgid "Manage Users"
 msgstr "管理使用者"
 
@@ -1181,10 +1224,11 @@ msgstr "執行模式"
 #: src/components/ChatGPT/ChatGPT.vue:248
 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
+#: src/views/config/ConfigList.vue:151
 msgid "Modify"
 msgstr "修改"
 
-#: src/routes/index.ts:147 src/views/certificate/CertificateEditor.vue:79
+#: src/routes/index.ts:158 src/views/certificate/CertificateEditor.vue:79
 #, fuzzy
 msgid "Modify Certificate"
 msgstr "憑證狀態"
@@ -1205,7 +1249,9 @@ msgstr "多行指令"
 #: src/views/certificate/ACMEUser.vue:13
 #: src/views/certificate/CertificateEditor.vue:152
 #: src/views/certificate/CertificateList/certColumns.tsx:10
-#: src/views/certificate/DNSCredential.vue:11 src/views/config/config.ts:7
+#: src/views/certificate/DNSCredential.vue:11
+#: src/views/config/components/Mkdir.vue:67 src/views/config/configColumns.ts:8
+#: src/views/config/ConfigEditor.vue:239
 #: src/views/domain/components/RightSettings.vue:83
 #: src/views/domain/components/SiteDuplicate.vue:129
 #: src/views/domain/DomainList.vue:13
@@ -1233,6 +1279,16 @@ msgstr "下載流量"
 msgid "Network Total Send"
 msgstr "上傳流量"
 
+#: src/views/config/components/Rename.vue:70
+#, fuzzy
+msgid "New name"
+msgstr "使用者名稱"
+
+#: src/views/config/ConfigEditor.vue:251
+#, fuzzy
+msgid "New Path"
+msgstr "路徑"
+
 #: src/views/system/Upgrade.vue:210
 msgid "New version released"
 msgstr "新版本發布"
@@ -1263,7 +1319,7 @@ msgstr "Nginx 控制元件"
 msgid "Nginx Error Log Path"
 msgstr "Nginx 錯誤日誌路徑"
 
-#: src/routes/index.ts:185 src/views/nginx_log/NginxLog.vue:143
+#: src/routes/index.ts:196 src/views/nginx_log/NginxLog.vue:143
 msgid "Nginx Log"
 msgstr "Nginx 日誌"
 
@@ -1298,7 +1354,7 @@ msgstr "Node Secret"
 msgid "Not After"
 msgstr ""
 
-#: src/routes/index.ts:300
+#: src/routes/index.ts:311
 msgid "Not Found"
 msgstr "找不到頁面"
 
@@ -1316,7 +1372,7 @@ msgstr "備註"
 msgid "Notification"
 msgstr "憑證"
 
-#: src/components/Notification/Notification.vue:82 src/routes/index.ts:231
+#: src/components/Notification/Notification.vue:82 src/routes/index.ts:242
 #, fuzzy
 msgid "Notifications"
 msgstr "憑證"
@@ -1385,6 +1441,10 @@ msgstr "線上"
 msgid "OpenAI"
 msgstr "OpenAI"
 
+#: src/views/config/components/Rename.vue:66
+msgid "Original name"
+msgstr ""
+
 #: src/views/system/Upgrade.vue:177
 msgid "OS"
 msgstr "作業系統"
@@ -1415,7 +1475,7 @@ msgstr "密碼"
 msgid "Password (*)"
 msgstr "密碼 (*)"
 
-#: src/views/config/ConfigEdit.vue:118
+#: src/views/config/ConfigEditor.vue:245
 #: src/views/domain/ngx_conf/LocationEditor.vue:118
 #: src/views/domain/ngx_conf/LocationEditor.vue:90
 msgid "Path"
@@ -1456,6 +1516,17 @@ msgstr ""
 "請先在 Certification > DNS Credentials 中新增認證,然後選擇以下認證之一以請"
 "求 DNS 供應商的 API。"
 
+#: src/views/config/components/Rename.vue:61
+#: src/views/config/ConfigEditor.vue:232
+#, fuzzy
+msgid "Please input a filename"
+msgstr "請輸入您的使用者名稱!"
+
+#: src/views/config/components/Mkdir.vue:59
+#, fuzzy
+msgid "Please input a folder name"
+msgstr "請輸入您的使用者名稱!"
+
 #: src/views/domain/components/SiteDuplicate.vue:38
 #: src/views/stream/components/StreamDuplicate.vue:38
 msgid ""
@@ -1491,7 +1562,7 @@ msgstr "請至少選擇一個節點!"
 msgid "Pre-release"
 msgstr "預先發布"
 
-#: src/routes/index.ts:249 src/views/preference/Preference.vue:105
+#: src/routes/index.ts:260 src/views/preference/Preference.vue:105
 msgid "Preference"
 msgstr "偏好設定"
 
@@ -1615,11 +1686,18 @@ msgstr "儲存成功"
 msgid "Removed successfully"
 msgstr "儲存成功"
 
+#: src/views/config/components/Rename.vue:52
+#: src/views/config/ConfigList.vue:159
 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
 #, fuzzy
 msgid "Rename"
 msgstr "使用者名稱"
 
+#: src/views/config/components/Rename.vue:37
+#, fuzzy
+msgid "Rename successfully"
+msgstr "啟用成功"
+
 #: src/views/certificate/RenewCert.vue:43
 #: src/views/certificate/RenewCert.vue:47
 #, fuzzy
@@ -1673,7 +1751,7 @@ msgstr "執行中"
 
 #: src/components/ChatGPT/ChatGPT.vue:251
 #: src/views/certificate/CertificateEditor.vue:254
-#: src/views/config/ConfigEdit.vue:96 src/views/domain/DomainEdit.vue:260
+#: src/views/config/ConfigEditor.vue:205 src/views/domain/DomainEdit.vue:260
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:120
 #: src/views/preference/Preference.vue:145 src/views/stream/StreamEdit.vue:252
 msgid "Save"
@@ -1683,7 +1761,7 @@ msgstr "儲存"
 msgid "Save Directive"
 msgstr "儲存指令"
 
-#: src/views/config/ConfigEdit.vue:57 src/views/domain/DomainAdd.vue:46
+#: src/views/config/ConfigEditor.vue:154 src/views/domain/DomainAdd.vue:46
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
 msgid "Save error %{msg}"
 msgstr "儲存錯誤 %{msg}"
@@ -1695,7 +1773,7 @@ msgstr "儲存錯誤 %{msg}"
 msgid "Save successfully"
 msgstr "儲存成功"
 
-#: src/views/config/ConfigEdit.vue:55 src/views/domain/DomainAdd.vue:37
+#: src/views/config/ConfigEditor.vue:150 src/views/domain/DomainAdd.vue:37
 #: src/views/domain/DomainEdit.vue:143
 #: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:39
 #: src/views/stream/StreamEdit.vue:138
@@ -1726,7 +1804,9 @@ msgstr "傳送"
 #: src/components/StdDesign/StdDataDisplay/StdBatchEdit.vue:42
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:213
 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:253
-#: src/views/config/ConfigEdit.vue:40 src/views/domain/DomainList.vue:81
+#: src/views/config/components/Mkdir.vue:38
+#: src/views/config/components/Rename.vue:40
+#: src/views/config/ConfigEditor.vue:93 src/views/domain/DomainList.vue:81
 #: src/views/environment/BatchUpgrader.vue:57
 #: src/views/environment/Environment.vue:15 src/views/other/Install.vue:68
 #: src/views/preference/AuthSettings.vue:49
@@ -1782,7 +1862,7 @@ msgstr ""
 msgid "Single Directive"
 msgstr "單一指令"
 
-#: src/routes/index.ts:207
+#: src/routes/index.ts:218
 msgid "Site Logs"
 msgstr "網站日誌"
 
@@ -1888,7 +1968,7 @@ msgstr "更換憑證"
 msgid "Sync to"
 msgstr ""
 
-#: src/routes/index.ts:258
+#: src/routes/index.ts:269
 msgid "System"
 msgstr "系統"
 
@@ -1901,7 +1981,7 @@ msgstr ""
 msgid "Target"
 msgstr "目標"
 
-#: src/routes/index.ts:177 src/views/pty/Terminal.vue:114
+#: src/routes/index.ts:188 src/views/pty/Terminal.vue:114
 msgid "Terminal"
 msgstr "終端機"
 
@@ -2055,18 +2135,19 @@ msgstr ""
 msgid "Trash"
 msgstr ""
 
-#: src/components/OTP/useOTPModal.ts:66
+#: src/components/OTP/useOTPModal.ts:70
 msgid "Two-factor authentication required"
 msgstr ""
 
 #: src/views/certificate/CertificateList/certColumns.tsx:25
-#: src/views/config/config.ts:12 src/views/notification/Notification.vue:13
+#: src/views/config/configColumns.ts:16
+#: src/views/notification/Notification.vue:13
 msgid "Type"
 msgstr "類型"
 
 #: src/views/certificate/ACMEUser.vue:53
-#: src/views/certificate/DNSCredential.vue:27 src/views/config/config.ts:27
-#: src/views/config/ConfigEdit.vue:121
+#: src/views/certificate/DNSCredential.vue:27
+#: src/views/config/configColumns.ts:31 src/views/config/ConfigEditor.vue:258
 #: src/views/domain/components/RightSettings.vue:86
 #: src/views/domain/DomainList.vue:41 src/views/environment/envColumns.tsx:124
 #: src/views/stream/components/RightSettings.vue:85
@@ -2078,7 +2159,7 @@ msgstr "更新時間"
 msgid "Updated successfully"
 msgstr "更新成功"
 
-#: src/routes/index.ts:273 src/views/environment/Environment.vue:50
+#: src/routes/index.ts:284 src/views/environment/Environment.vue:50
 #: src/views/system/Upgrade.vue:145 src/views/system/Upgrade.vue:228
 msgid "Upgrade"
 msgstr "升級"
@@ -2209,6 +2290,10 @@ msgstr "您正在使用最新版本"
 msgid "You can check Nginx UI upgrade at this page."
 msgstr "您可以在此頁面檢查 Nginx UI 的升級。"
 
+#, fuzzy
+#~ msgid "Rename "
+#~ msgstr "使用者名稱"
+
 #~ msgid "Auto Cert"
 #~ msgstr "自動憑證"
 

+ 4 - 0
app/src/layouts/BaseLayout.vue

@@ -28,6 +28,10 @@ const { server_name } = storeToRefs(useSettingsStore())
 settings.get_server_name().then(r => {
   server_name.value = r.name
 })
+
+const breadList = ref([])
+
+provide('breadList', breadList)
 </script>
 
 <template>

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

@@ -1,15 +1,17 @@
 import type { AxiosRequestConfig } from 'axios'
 import axios from 'axios'
+import { useCookies } from '@vueuse/integrations/useCookies'
 import { storeToRefs } from 'pinia'
 import NProgress from 'nprogress'
 import { useSettingsStore, useUserStore } from '@/pinia'
 import 'nprogress/nprogress.css'
 
 import router from '@/routes'
+import useOTPModal from '@/components/OTP/useOTPModal'
 
 const user = useUserStore()
 const settings = useSettingsStore()
-const { token } = storeToRefs(user)
+const { token, secureSessionId } = storeToRefs(user)
 
 const instance = axios.create({
   baseURL: import.meta.env.VITE_API_ROOT,
@@ -28,7 +30,7 @@ const instance = axios.create({
 instance.interceptors.request.use(
   config => {
     NProgress.start()
-    if (token) {
+    if (token.value) {
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       (config.headers as any).Authorization = token.value
     }
@@ -38,6 +40,11 @@ instance.interceptors.request.use(
       (config.headers as any)['X-Node-ID'] = settings.environment.id
     }
 
+    if (secureSessionId.value) {
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      (config.headers as any)['X-Secure-Session-ID'] = secureSessionId.value
+    }
+
     return config
   },
   err => {
@@ -53,8 +60,14 @@ instance.interceptors.response.use(
   },
   async error => {
     NProgress.done()
+
+    const otpModal = useOTPModal()
+    const cookies = useCookies(['nginx-ui-2fa'])
     switch (error.response.status) {
       case 401:
+        cookies.remove('secure_session_id')
+        await otpModal.open()
+        break
       case 403:
         user.logout()
         await router.push('/login')

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

@@ -4,6 +4,7 @@ export const useUserStore = defineStore('user', {
   state: () => ({
     token: '',
     unreadCount: 0,
+    secureSessionId: '',
   }),
   getters: {
     is_login(state): boolean {

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

@@ -97,20 +97,31 @@ export const routes: RouteRecordRaw[] = [
       {
         path: 'config',
         name: 'Manage Configs',
-        component: () => import('@/views/config/Config.vue'),
+        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/ConfigEdit.vue'),
+        component: () => import('@/views/config/ConfigEditor.vue'),
         meta: {
           name: () => $gettext('Edit Configuration'),
           hiddenInSidebar: true,
+          lastRouteName: 'Manage Configs',
         },
       },
       {

+ 1 - 1
app/src/version.json

@@ -1 +1 @@
-{"version":"2.0.0-beta.28","build_id":147,"total_build":351}
+{"version":"2.0.0-beta.28","build_id":149,"total_build":353}

+ 0 - 80
app/src/views/config/Config.vue

@@ -1,80 +0,0 @@
-<script setup lang="ts">
-import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
-import config from '@/api/config'
-import configColumns from '@/views/config/config'
-import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
-import router from '@/routes'
-import InspectConfig from '@/views/config/InspectConfig.vue'
-
-const api = config
-
-const table = ref(null)
-const route = useRoute()
-
-const basePath = computed(() => {
-  let dir = route?.query?.dir ?? ''
-  if (dir)
-    dir += '/'
-
-  return dir
-})
-
-const getParams = computed(() => {
-  return {
-    dir: basePath.value,
-  }
-})
-
-const update = ref(1)
-
-watch(getParams, () => {
-  update.value++
-})
-
-const refInspectConfig = ref()
-
-watch(route, () => {
-  refInspectConfig.value?.test()
-})
-</script>
-
-<template>
-  <ACard :title="$gettext('Configurations')">
-    <InspectConfig ref="refInspectConfig" />
-    <StdTable
-      :key="update"
-      ref="table"
-      :api="api"
-      :columns="configColumns"
-      disable-delete
-      disable-search
-      disable-view
-      row-key="name"
-      :get-params="getParams"
-      disable-query-params
-      @click-edit="(r, row) => {
-        if (!row.is_dir) {
-          $router.push({
-            path: `/config/${basePath}${r}/edit`,
-          })
-        }
-        else {
-          $router.push({
-            query: {
-              dir: basePath + r,
-            },
-          })
-        }
-      }"
-    />
-    <FooterToolBar v-if="basePath">
-      <AButton @click="router.go(-1)">
-        {{ $gettext('Back') }}
-      </AButton>
-    </FooterToolBar>
-  </ACard>
-</template>
-
-<style scoped>
-
-</style>

+ 0 - 160
app/src/views/config/ConfigEdit.vue

@@ -1,160 +0,0 @@
-<script setup lang="ts">
-import { useRoute } from 'vue-router'
-import { message } from 'ant-design-vue'
-import type { Ref } from 'vue'
-import { formatDateTime } from '@/lib/helper'
-import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
-import config from '@/api/config'
-import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
-import ngx from '@/api/ngx'
-import InspectConfig from '@/views/config/InspectConfig.vue'
-import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
-import type { ChatComplicationMessage } from '@/api/openai'
-
-const route = useRoute()
-
-const inspect_config = ref()
-
-const name = computed(() => {
-  const n = route.params.name
-  if (typeof n === 'string')
-    return n
-
-  return n?.join('/')
-})
-
-const configText = ref('')
-const history_chatgpt_record = ref([]) as Ref<ChatComplicationMessage[]>
-const filepath = ref('')
-const active_key = ref(['1', '2'])
-const modified_at = ref('')
-
-function init() {
-  if (name.value) {
-    config.get(name.value).then(r => {
-      configText.value = r.content
-      history_chatgpt_record.value = r.chatgpt_messages
-      filepath.value = r.filepath
-      modified_at.value = r.modified_at
-    }).catch(r => {
-      message.error(r.message ?? $gettext('Server error'))
-    })
-  }
-  else {
-    configText.value = ''
-    history_chatgpt_record.value = []
-    filepath.value = ''
-  }
-}
-
-init()
-
-function save() {
-  config.save(name.value, { content: configText.value }).then(r => {
-    configText.value = r.content
-    message.success($gettext('Saved successfully'))
-  }).catch(r => {
-    message.error($gettext('Save error %{msg}', { msg: r.message ?? '' }))
-  }).finally(() => {
-    inspect_config.value.test()
-  })
-}
-
-function format_code() {
-  ngx.format_code(configText.value).then(r => {
-    configText.value = r.content
-    message.success($gettext('Format successfully'))
-  }).catch(r => {
-    message.error($gettext('Format error %{msg}', { msg: r.message ?? '' }))
-  })
-}
-
-</script>
-
-<template>
-  <ARow :gutter="16">
-    <ACol
-      :xs="24"
-      :sm="24"
-      :md="18"
-    >
-      <ACard :title="$gettext('Edit Configuration')">
-        <InspectConfig ref="inspect_config" />
-        <CodeEditor v-model:content="configText" />
-        <FooterToolBar>
-          <ASpace>
-            <AButton @click="$router.go(-1)">
-              {{ $gettext('Back') }}
-            </AButton>
-            <AButton @click="format_code">
-              {{ $gettext('Format Code') }}
-            </AButton>
-            <AButton
-              type="primary"
-              @click="save"
-            >
-              {{ $gettext('Save') }}
-            </AButton>
-          </ASpace>
-        </FooterToolBar>
-      </ACard>
-    </ACol>
-
-    <ACol
-      :xs="24"
-      :sm="24"
-      :md="6"
-    >
-      <ACard class="col-right">
-        <ACollapse
-          v-model:activeKey="active_key"
-          ghost
-        >
-          <ACollapsePanel
-            key="1"
-            :header="$gettext('Basic')"
-          >
-            <AForm layout="vertical">
-              <AFormItem :label="$gettext('Path')">
-                {{ filepath }}
-              </AFormItem>
-              <AFormItem :label="$gettext('Updated at')">
-                {{ formatDateTime(modified_at) }}
-              </AFormItem>
-            </AForm>
-          </ACollapsePanel>
-          <ACollapsePanel
-            key="2"
-            header="ChatGPT"
-          >
-            <ChatGPT
-              v-model:history-messages="history_chatgpt_record"
-              :content="configText"
-              :path="filepath"
-            />
-          </ACollapsePanel>
-        </ACollapse>
-      </ACard>
-    </ACol>
-  </ARow>
-</template>
-
-<style lang="less" scoped>
-.col-right {
-  position: sticky;
-  top: 78px;
-
-  :deep(.ant-card-body) {
-    max-height: 100vh;
-    overflow-y: scroll;
-  }
-}
-
-:deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
-  padding: 0;
-}
-
-:deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
-  padding: 0 0 10px 0;
-}
-</style>

+ 345 - 0
app/src/views/config/ConfigEditor.vue

@@ -0,0 +1,345 @@
+<script setup lang="ts">
+import { message } from 'ant-design-vue'
+import type { Ref } from 'vue'
+import { InfoCircleOutlined } from '@ant-design/icons-vue'
+import { formatDateTime } from '@/lib/helper'
+import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
+import type { Config } from '@/api/config'
+import config from '@/api/config'
+import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
+import ngx from '@/api/ngx'
+import InspectConfig from '@/views/config/InspectConfig.vue'
+import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
+import type { ChatComplicationMessage } from '@/api/openai'
+import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
+import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
+import { useSettingsStore } from '@/pinia'
+
+const settings = useSettingsStore()
+const route = useRoute()
+const router = useRouter()
+const refForm = ref()
+const refInspectConfig = ref()
+const origName = ref('')
+const addMode = computed(() => !route.params.name)
+const errors = ref({})
+
+const basePath = computed(() => {
+  if (route.query.basePath)
+    return route?.query?.basePath?.toString().replaceAll('/', '')
+  else if (typeof route.params.name === 'object')
+    return (route.params.name as string[]).slice(0, -1).join('/')
+  else
+    return ''
+})
+
+const data = ref({
+  name: '',
+  content: '',
+  filepath: '',
+  sync_node_ids: [] as number[],
+  sync_overwrite: false,
+} as Config)
+
+const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]>
+const activeKey = ref(['basic', 'deploy', 'chatgpt'])
+const modifiedAt = ref('')
+const nginxConfigBase = ref('')
+
+const newPath = computed(() => [nginxConfigBase.value, basePath.value, data.value.name]
+  .filter(v => v)
+  .join('/'))
+
+const relativePath = computed(() => (route.params.name as string[]).join('/'))
+const breadcrumbs = useBreadcrumbs()
+
+async function init() {
+  const { name } = route.params
+
+  data.value.name = name?.[name?.length - 1] ?? ''
+  origName.value = data.value.name
+  if (!addMode.value) {
+    config.get(relativePath.value).then(r => {
+      data.value = r
+      historyChatgptRecord.value = r.chatgpt_messages
+      modifiedAt.value = r.modified_at
+
+      const path = data.value.filepath
+        .replaceAll(`${nginxConfigBase.value}/`, '')
+        .replaceAll(data.value.name, '')
+        .split('/')
+        .filter(v => v)
+        .map(v => {
+          return {
+            name: 'Manage Configs',
+            translatedName: () => v,
+            path: '/config',
+            query: {
+              dir: v,
+            },
+            hasChildren: false,
+          }
+        })
+
+      breadcrumbs.value = [{
+        name: 'Dashboard',
+        translatedName: () => $gettext('Dashboard'),
+        path: '/dashboard',
+        hasChildren: false,
+      }, {
+        name: 'Manage Configs',
+        translatedName: () => $gettext('Manage Configs'),
+        path: '/config',
+        hasChildren: false,
+      }, ...path, {
+        name: 'Edit Config',
+        translatedName: () => origName.value,
+        hasChildren: false,
+      }]
+    }).catch(r => {
+      message.error(r.message ?? $gettext('Server error'))
+    })
+  }
+  else {
+    data.value.content = ''
+    historyChatgptRecord.value = []
+    data.value.filepath = ''
+
+    const path = basePath.value
+      .split('/')
+      .filter(v => v)
+      .map(v => {
+        return {
+          name: 'Manage Configs',
+          translatedName: () => v,
+          path: '/config',
+          query: {
+            dir: v,
+          },
+          hasChildren: false,
+        }
+      })
+
+    breadcrumbs.value = [{
+      name: 'Dashboard',
+      translatedName: () => $gettext('Dashboard'),
+      path: '/dashboard',
+      hasChildren: false,
+    }, {
+      name: 'Manage Configs',
+      translatedName: () => $gettext('Manage Configs'),
+      path: '/config',
+      hasChildren: false,
+    }, ...path, {
+      name: 'Add Config',
+      translatedName: () => $gettext('Add Configuration'),
+      hasChildren: false,
+    }]
+  }
+}
+
+onMounted(async () => {
+  await config.get_base_path().then(r => {
+    nginxConfigBase.value = r.base_path
+  })
+  await init()
+})
+
+function save() {
+  refForm.value.validate().then(() => {
+    config.save(addMode.value ? null : relativePath.value, {
+      name: data.value.name,
+      filepath: data.value.filepath,
+      new_filepath: newPath.value,
+      content: data.value.content,
+      sync_node_ids: data.value.sync_node_ids,
+      sync_overwrite: data.value.sync_overwrite,
+    }).then(r => {
+      data.value.content = r.content
+      message.success($gettext('Saved successfully'))
+      router.push(`/config/${r.filepath.replaceAll(`${nginxConfigBase.value}/`, '')}/edit`)
+    }).catch(e => {
+      errors.value = e.errors
+      message.error($gettext('Save error %{msg}', { msg: e.message ?? '' }))
+    }).finally(() => {
+      refInspectConfig.value.test()
+    })
+  })
+}
+
+function formatCode() {
+  ngx.format_code(data.value.content).then(r => {
+    data.value.content = r.content
+    message.success($gettext('Format successfully'))
+  }).catch(r => {
+    message.error($gettext('Format error %{msg}', { msg: r.message ?? '' }))
+  })
+}
+
+function goBack() {
+  router.push({
+    path: '/config',
+    query: {
+      dir: basePath.value || undefined,
+    },
+  })
+}
+</script>
+
+<template>
+  <ARow :gutter="16">
+    <ACol
+      :xs="24"
+      :sm="24"
+      :md="18"
+    >
+      <ACard :title="addMode ? $gettext('Add Configuration') : $gettext('Edit Configuration')">
+        <InspectConfig
+          v-show="!addMode"
+          ref="refInspectConfig"
+        />
+        <CodeEditor v-model:content="data.content" />
+        <FooterToolBar>
+          <ASpace>
+            <AButton @click="goBack">
+              {{ $gettext('Back') }}
+            </AButton>
+            <AButton @click="formatCode">
+              {{ $gettext('Format Code') }}
+            </AButton>
+            <AButton
+              type="primary"
+              @click="save"
+            >
+              {{ $gettext('Save') }}
+            </AButton>
+          </ASpace>
+        </FooterToolBar>
+      </ACard>
+    </ACol>
+
+    <ACol
+      :xs="24"
+      :sm="24"
+      :md="6"
+    >
+      <ACard class="col-right">
+        <ACollapse
+          v-model:activeKey="activeKey"
+          ghost
+        >
+          <ACollapsePanel
+            key="basic"
+            :header="$gettext('Basic')"
+          >
+            <AForm
+              ref="refForm"
+              layout="vertical"
+              :model="data"
+              :rules="{
+                name: [
+                  { required: true, message: $gettext('Please input a filename') },
+                  { pattern: /^[^\\/]+$/, message: $gettext('Invalid filename') },
+                ],
+              }"
+            >
+              <AFormItem
+                name="name"
+                :label="$gettext('Name')"
+              >
+                <AInput v-model:value="data.name" />
+              </AFormItem>
+              <AFormItem
+                v-if="!addMode"
+                :label="$gettext('Path')"
+              >
+                {{ data.filepath }}
+              </AFormItem>
+              <AFormItem
+                v-show="data.name !== origName"
+                :label="addMode ? $gettext('New Path') : $gettext('Changed Path')"
+                required
+              >
+                {{ newPath }}
+              </AFormItem>
+              <AFormItem
+                v-if="!addMode"
+                :label="$gettext('Updated at')"
+              >
+                {{ formatDateTime(modifiedAt) }}
+              </AFormItem>
+            </AForm>
+          </ACollapsePanel>
+          <ACollapsePanel
+            v-if="!settings.is_remote"
+            key="deploy"
+            :header="$gettext('Deploy')"
+          >
+            <NodeSelector
+              v-model:target="data.sync_node_ids"
+              hidden-local
+            />
+            <div class="node-deploy-control">
+              <div class="overwrite">
+                <ACheckbox v-model:checked="data.sync_overwrite">
+                  {{ $gettext('Overwrite') }}
+                </ACheckbox>
+                <ATooltip placement="bottom">
+                  <template #title>
+                    {{ $gettext('Overwrite exist file') }}
+                  </template>
+                  <InfoCircleOutlined />
+                </ATooltip>
+              </div>
+            </div>
+          </ACollapsePanel>
+          <ACollapsePanel
+            key="chatgpt"
+            header="ChatGPT"
+          >
+            <ChatGPT
+              v-model:history-messages="historyChatgptRecord"
+              :content="data.content"
+              :path="data.filepath"
+            />
+          </ACollapsePanel>
+        </ACollapse>
+      </ACard>
+    </ACol>
+  </ARow>
+</template>
+
+<style lang="less" scoped>
+.col-right {
+  position: sticky;
+  top: 78px;
+
+  :deep(.ant-card-body) {
+    max-height: 100vh;
+    overflow-y: scroll;
+  }
+}
+
+:deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
+  padding: 0;
+}
+
+:deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
+  padding: 0 0 10px 0;
+}
+
+.overwrite {
+  margin-right: 15px;
+
+  span {
+    color: #9b9b9b;
+  }
+}
+
+.node-deploy-control {
+  display: flex;
+  justify-content: flex-end;
+  margin-top: 10px;
+  align-items: center;
+}
+</style>

+ 181 - 0
app/src/views/config/ConfigList.vue

@@ -0,0 +1,181 @@
+<script setup lang="ts">
+import { $gettext } from '../../gettext'
+import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
+import config from '@/api/config'
+import configColumns from '@/views/config/configColumns'
+import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
+import InspectConfig from '@/views/config/InspectConfig.vue'
+import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
+import Mkdir from '@/views/config/components/Mkdir.vue'
+import Rename from '@/views/config/components/Rename.vue'
+
+const table = ref()
+const route = useRoute()
+const router = useRouter()
+
+const basePath = computed(() => {
+  let dir = route?.query?.dir ?? ''
+  if (dir)
+    dir += '/'
+
+  return dir as string
+})
+
+const getParams = computed(() => {
+  return {
+    dir: basePath.value,
+  }
+})
+
+const update = ref(1)
+
+watch(getParams, () => {
+  update.value++
+})
+
+const refInspectConfig = ref()
+const breadcrumbs = useBreadcrumbs()
+
+function updateBreadcrumbs() {
+  const path = basePath.value
+    .split('/')
+    .filter(v => v)
+    .map(v => {
+      return {
+        name: 'Manage Configs',
+        translatedName: () => v,
+        path: '/config',
+        query: {
+          dir: v,
+        },
+        hasChildren: false,
+      }
+    })
+
+  breadcrumbs.value = [{
+    name: 'Dashboard',
+    translatedName: () => $gettext('Dashboard'),
+    path: '/dashboard',
+    hasChildren: false,
+  }, {
+    name: 'Manage Configs',
+    translatedName: () => $gettext('Manage Configs'),
+    path: '/config',
+    hasChildren: false,
+  }, ...path]
+}
+
+onMounted(() => {
+  updateBreadcrumbs()
+})
+
+watch(route, () => {
+  refInspectConfig.value?.test()
+  updateBreadcrumbs()
+})
+
+function goBack() {
+  router.push({
+    path: '/config',
+    query: {
+      dir: `${basePath.value.split('/').slice(0, -2).join('/')}` || undefined,
+    },
+  })
+}
+
+const refMkdir = ref()
+const refRename = ref()
+</script>
+
+<template>
+  <ACard :title="$gettext('Configurations')">
+    <template #extra>
+      <AButton
+        v-if="basePath"
+        type="link"
+        size="small"
+        @click="goBack"
+      >
+        {{ $gettext('Back') }}
+      </AButton>
+      <AButton
+        type="link"
+        size="small"
+        @click="router.push({
+          path: '/config/add',
+          query: { basePath: basePath || undefined },
+        })"
+      >
+        {{ $gettext('Create File') }}
+      </AButton>
+      <AButton
+        type="link"
+        size="small"
+        @click="() => refMkdir.open(basePath)"
+      >
+        {{ $gettext('Create Folder') }}
+      </AButton>
+    </template>
+    <InspectConfig ref="refInspectConfig" />
+    <StdTable
+      :key="update"
+      ref="table"
+      :api="config"
+      :columns="configColumns"
+      disable-delete
+      disable-view
+      row-key="name"
+      :get-params="getParams"
+      disable-query-params
+      disable-modify
+    >
+      <template #actions="{ record }">
+        <AButton
+          type="link"
+          size="small"
+          @click="() => {
+            if (!record.is_dir) {
+              $router.push({
+                path: `/config/${basePath}${record.name}/edit`,
+              })
+            }
+            else {
+              $router.push({
+                query: {
+                  dir: basePath + record.name,
+                },
+              })
+            }
+          }"
+        >
+          {{ $gettext('Modify') }}
+        </AButton>
+        <ADivider type="vertical" />
+        <AButton
+          type="link"
+          size="small"
+          @click="() => refRename.open(basePath, record.name, record.is_dir)"
+        >
+          {{ $gettext('Rename') }}
+        </AButton>
+      </template>
+    </StdTable>
+    <Mkdir
+      ref="refMkdir"
+      @created="() => table.get_list()"
+    />
+    <Rename
+      ref="refRename"
+      @renamed="() => table.get_list()"
+    />
+    <FooterToolBar v-if="basePath">
+      <AButton @click="goBack">
+        {{ $gettext('Back') }}
+      </AButton>
+    </FooterToolBar>
+  </ACard>
+</template>
+
+<style scoped>
+
+</style>

+ 74 - 0
app/src/views/config/components/Mkdir.vue

@@ -0,0 +1,74 @@
+<script setup lang="ts">
+
+import { message } from 'ant-design-vue'
+import config from '@/api/config'
+import useOTPModal from '@/components/OTP/useOTPModal'
+
+const emit = defineEmits(['created'])
+const visible = ref(false)
+
+const data = ref({
+  basePath: '',
+  name: '',
+})
+
+const refForm = ref()
+function open(basePath: string) {
+  visible.value = true
+  data.value.name = ''
+  data.value.basePath = basePath
+}
+
+defineExpose({
+  open,
+})
+
+function ok() {
+  refForm.value.validate().then(() => {
+    const otpModal = useOTPModal()
+
+    otpModal.open().then(() => {
+      config.mkdir(data.value.basePath, data.value.name).then(() => {
+        visible.value = false
+
+        message.success($gettext('Created successfully'))
+        emit('created')
+      }).catch(e => {
+        message.error(`${$gettext('Server error')} ${e?.message}`)
+      })
+    })
+  })
+}
+</script>
+
+<template>
+  <AModal
+    v-model:open="visible"
+    :mask="false"
+    :title="$gettext('Create Folder')"
+    @ok="ok"
+  >
+    <AForm
+      ref="refForm"
+      layout="vertical"
+      :model="data"
+      :rules="{
+        name: [
+          { required: true, message: $gettext('Please input a folder name') },
+          { pattern: /^[^\\/]+$/, message: $gettext('Invalid folder name') },
+        ],
+      }"
+    >
+      <AFormItem name="name">
+        <AInput
+          v-model:value="data.name"
+          :placeholder="$gettext('Name')"
+        />
+      </AFormItem>
+    </AForm>
+  </AModal>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 93 - 0
app/src/views/config/components/Rename.vue

@@ -0,0 +1,93 @@
+<script setup lang="ts">
+import { message } from 'ant-design-vue'
+import config from '@/api/config'
+import useOTPModal from '@/components/OTP/useOTPModal'
+import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
+
+const emit = defineEmits(['renamed'])
+const visible = ref(false)
+const isDirFlag = ref(false)
+
+const data = ref({
+  basePath: '',
+  orig_name: '',
+  new_name: '',
+  sync_node_ids: [] as number[],
+})
+
+const refForm = ref()
+
+function open(basePath: string, origName: string, isDir: boolean) {
+  visible.value = true
+  data.value.orig_name = origName
+  data.value.new_name = origName
+  data.value.basePath = basePath
+  isDirFlag.value = isDir
+}
+
+defineExpose({
+  open,
+})
+
+function ok() {
+  refForm.value.validate().then(() => {
+    const { basePath, orig_name, new_name, sync_node_ids } = data.value
+
+    const otpModal = useOTPModal()
+
+    otpModal.open().then(() => {
+      config.rename(basePath, orig_name, new_name, sync_node_ids).then(() => {
+        visible.value = false
+        message.success($gettext('Rename successfully'))
+        emit('renamed')
+      }).catch(e => {
+        message.error(`${$gettext('Server error')} ${e?.message}`)
+      })
+    })
+  })
+}
+</script>
+
+<template>
+  <AModal
+    v-model:open="visible"
+    :mask="false"
+    :title="$gettext('Rename')"
+    @ok="ok"
+  >
+    <AForm
+      ref="refForm"
+      layout="vertical"
+      :model="data"
+      :rules="{
+        new_name: [
+          { required: true, message: $gettext('Please input a filename') },
+          { pattern: /^[^\\/]+$/, message: $gettext('Invalid filename') },
+        ],
+      }"
+    >
+      <AFormItem :label="$gettext('Original name')">
+        <p>{{ data.orig_name }}</p>
+      </AFormItem>
+      <AFormItem
+        :label="$gettext('New name')"
+        name="new_name"
+      >
+        <AInput v-model:value="data.new_name" />
+      </AFormItem>
+      <AFormItem
+        v-if="isDirFlag"
+        :label="$gettext('Sync')"
+      >
+        <NodeSelector
+          v-model:target="data.sync_node_ids"
+          hidden-local
+        />
+      </AFormItem>
+    </AForm>
+  </AModal>
+</template>
+
+<style scoped lang="less">
+
+</style>

+ 4 - 0
app/src/views/config/config.ts → app/src/views/config/configColumns.ts

@@ -2,12 +2,16 @@ import { h } from 'vue'
 import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
 import type { JSXElements } from '@/components/StdDesign/types'
+import { input } from '@/components/StdDesign/StdDataEntry'
 
 const configColumns = [{
   title: () => $gettext('Name'),
   dataIndex: 'name',
   sorter: true,
   pithy: true,
+  search: {
+    type: input,
+  },
 }, {
   title: () => $gettext('Type'),
   dataIndex: 'is_dir',

+ 4 - 36
app/src/views/notification/Notification.vue

@@ -2,41 +2,8 @@
 import { message } from 'ant-design-vue'
 import StdCurd from '@/components/StdDesign/StdDataDisplay/StdCurd.vue'
 import notification from '@/api/notification'
-import type { Column } from '@/components/StdDesign/types'
-import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
-import { datetime, mask } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
-import { NotificationType } from '@/constants'
 import { useUserStore } from '@/pinia'
-import { detailRender } from '@/components/Notification/detailRender'
-
-const columns: Column[] = [{
-  title: () => $gettext('Type'),
-  dataIndex: 'type',
-  customRender: mask(NotificationType),
-  sortable: true,
-  pithy: true,
-}, {
-  title: () => $gettext('Title'),
-  dataIndex: 'title',
-  customRender: (args: customRender) => {
-    return h('span', $gettext(args.text))
-  },
-  pithy: true,
-}, {
-  title: () => $gettext('Details'),
-  dataIndex: 'details',
-  customRender: detailRender,
-  pithy: true,
-}, {
-  title: () => $gettext('Created at'),
-  dataIndex: 'created_at',
-  sortable: true,
-  customRender: datetime,
-  pithy: true,
-}, {
-  title: () => $gettext('Action'),
-  dataIndex: 'action',
-}]
+import notificationColumns from '@/views/notification/notificationColumns'
 
 const { unreadCount } = storeToRefs(useUserStore())
 
@@ -60,10 +27,11 @@ watch(unreadCount, () => {
   <StdCurd
     ref="curd"
     :title="$gettext('Notification')"
-    :columns="columns"
+    :columns="notificationColumns"
     :api="notification"
-    disabled-modify
+    disable-modify
     disable-add
+    disable-trash
   >
     <template #extra>
       <APopconfirm

+ 58 - 0
app/src/views/notification/notificationColumns.tsx

@@ -0,0 +1,58 @@
+import { Tag } from 'ant-design-vue'
+import type { Column } from '@/components/StdDesign/types'
+import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
+import { datetime } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer'
+import { NotificationTypeT } from '@/constants'
+import { detailRender } from '@/components/Notification/detailRender'
+
+const columns: Column[] = [{
+  title: () => $gettext('Type'),
+  dataIndex: 'type',
+  customRender: (args: customRender) => {
+    if (args.text === NotificationTypeT.Error) {
+      return <Tag color="error">
+        { $gettext('Error') }
+      </Tag>
+    }
+    else if (args.text === NotificationTypeT.Warning) {
+      return <Tag color="warning">
+      { $gettext('Warning') }
+    </Tag>
+    }
+    else if (args.text === NotificationTypeT.Info) {
+      return <Tag color="info">
+      { $gettext('Info')}
+    </Tag>
+    }
+    else if (args.text === NotificationTypeT.Success) {
+      return <Tag color="success">
+      { $gettext('Success') }
+    </Tag>
+    }
+  },
+  sortable: true,
+  pithy: true,
+}, {
+  title: () => $gettext('Title'),
+  dataIndex: 'title',
+  customRender: (args: customRender) => {
+    return h('span', $gettext(args.text))
+  },
+  pithy: true,
+}, {
+  title: () => $gettext('Details'),
+  dataIndex: 'details',
+  customRender: detailRender,
+  pithy: true,
+}, {
+  title: () => $gettext('Created at'),
+  dataIndex: 'created_at',
+  sortable: true,
+  customRender: datetime,
+  pithy: true,
+}, {
+  title: () => $gettext('Action'),
+  dataIndex: 'action',
+}]
+
+export default columns

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

@@ -5,6 +5,7 @@ import { FitAddon } from '@xterm/addon-fit'
 import _ from 'lodash'
 import ws from '@/lib/websocket'
 import useOTPModal from '@/components/OTP/useOTPModal'
+import otp from '@/api/otp'
 
 let term: Terminal | null
 let ping: NodeJS.Timeout
@@ -14,30 +15,29 @@ const websocket = shallowRef()
 const lostConnection = ref(false)
 
 onMounted(() => {
+  otp.secure_session_status()
+
   const otpModal = useOTPModal()
 
-  otpModal.open({
-    onOk(secureSessionId: string) {
-      websocket.value = ws(`/api/pty?X-Secure-Session-ID=${secureSessionId}`, false)
-
-      nextTick(() => {
-        initTerm()
-        websocket.value.onmessage = wsOnMessage
-        websocket.value.onopen = wsOnOpen
-        websocket.value.onerror = () => {
-          lostConnection.value = true
-        }
-        websocket.value.onclose = () => {
-          lostConnection.value = true
-        }
-      })
-    },
-    onCancel() {
-      if (window.history.length > 1)
-        router.go(-1)
-      else
-        router.push('/')
-    },
+  otpModal.open().then(secureSessionId => {
+    websocket.value = ws(`/api/pty?X-Secure-Session-ID=${secureSessionId}`, false)
+
+    nextTick(() => {
+      initTerm()
+      websocket.value.onmessage = wsOnMessage
+      websocket.value.onopen = wsOnOpen
+      websocket.value.onerror = () => {
+        lostConnection.value = true
+      }
+      websocket.value.onclose = () => {
+        lostConnection.value = true
+      }
+    })
+  }).catch(() => {
+    if (window.history.length > 1)
+      router.go(-1)
+    else
+      router.push('/')
   })
 })
 

+ 1 - 1
app/version.json

@@ -1 +1 @@
-{"version":"2.0.0-beta.28","build_id":147,"total_build":351}
+{"version":"2.0.0-beta.28","build_id":149,"total_build":353}

+ 1 - 1
go.mod

@@ -1,6 +1,6 @@
 module github.com/0xJacky/Nginx-UI
 
-go 1.22.0
+go 1.22.5
 
 require (
 	github.com/0xJacky/pofile v0.2.1

+ 1 - 1
internal/config/config.go

@@ -7,7 +7,7 @@ import (
 
 type Config struct {
 	Name            string                         `json:"name"`
-	Content         string                         `json:"content,omitempty"`
+	Content         string                         `json:"content"`
 	ChatGPTMessages []openai.ChatCompletionMessage `json:"chatgpt_messages,omitempty"`
 	FilePath        string                         `json:"filepath,omitempty"`
 	ModifiedAt      time.Time                      `json:"modified_at"`

+ 257 - 0
internal/config/sync.go

@@ -0,0 +1,257 @@
+package config
+
+import (
+	"bytes"
+	"crypto/tls"
+	"encoding/json"
+	"fmt"
+	"github.com/0xJacky/Nginx-UI/internal/helper"
+	"github.com/0xJacky/Nginx-UI/internal/logger"
+	"github.com/0xJacky/Nginx-UI/internal/nginx"
+	"github.com/0xJacky/Nginx-UI/internal/notification"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/0xJacky/Nginx-UI/query"
+	"github.com/gin-gonic/gin"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+type SyncConfigPayload struct {
+	Name        string `json:"name"`
+	Filepath    string `json:"filepath"`
+	NewFilepath string `json:"new_filepath"`
+	Content     string `json:"content"`
+	Overwrite   bool   `json:"overwrite"`
+}
+
+func SyncToRemoteServer(c *model.Config, newFilepath string) (err error) {
+	if c.Filepath == "" || len(c.SyncNodeIds) == 0 {
+		return
+	}
+
+	nginxConfPath := nginx.GetConfPath()
+	if !helper.IsUnderDirectory(c.Filepath, nginxConfPath) {
+		return fmt.Errorf("config: %s is not under the nginx conf path: %s",
+			c.Filepath, nginxConfPath)
+	}
+
+	if newFilepath != "" && !helper.IsUnderDirectory(newFilepath, nginxConfPath) {
+		return fmt.Errorf("config: %s is not under the nginx conf path: %s",
+			c.Filepath, nginxConfPath)
+	}
+
+	currentPath := c.Filepath
+	if newFilepath != "" {
+		currentPath = newFilepath
+	}
+	configBytes, err := os.ReadFile(currentPath)
+	if err != nil {
+		return
+	}
+
+	payload := &SyncConfigPayload{
+		Name:        c.Name,
+		Filepath:    c.Filepath,
+		NewFilepath: newFilepath,
+		Content:     string(configBytes),
+		Overwrite:   c.SyncOverwrite,
+	}
+	payloadBytes, err := json.Marshal(payload)
+	if err != nil {
+		return
+	}
+
+	q := query.Environment
+	envs, _ := q.Where(q.ID.In(c.SyncNodeIds...)).Find()
+	for _, env := range envs {
+		go func() {
+			err := payload.deploy(env, c, payloadBytes)
+			if err != nil {
+				logger.Error(err)
+			}
+		}()
+	}
+
+	return
+}
+
+func SyncRenameOnRemoteServer(origPath, newPath string, syncNodeIds []int) (err error) {
+	if origPath == "" || newPath == "" || len(syncNodeIds) == 0 {
+		return
+	}
+
+	nginxConfPath := nginx.GetConfPath()
+	if !helper.IsUnderDirectory(origPath, nginxConfPath) {
+		return fmt.Errorf("config: %s is not under the nginx conf path: %s",
+			origPath, nginxConfPath)
+	}
+
+	if !helper.IsUnderDirectory(newPath, nginxConfPath) {
+		return fmt.Errorf("config: %s is not under the nginx conf path: %s",
+			newPath, nginxConfPath)
+	}
+
+	payload := &RenameConfigPayload{
+		Filepath:    origPath,
+		NewFilepath: newPath,
+	}
+
+	q := query.Environment
+	envs, _ := q.Where(q.ID.In(syncNodeIds...)).Find()
+	for _, env := range envs {
+		go func() {
+			err := payload.rename(env)
+			if err != nil {
+				logger.Error(err)
+			}
+		}()
+	}
+
+	return
+}
+
+type SyncNotificationPayload struct {
+	StatusCode int    `json:"status_code"`
+	ConfigName string `json:"config_name"`
+	EnvName    string `json:"env_name"`
+	RespBody   string `json:"resp_body"`
+}
+
+func (p *SyncConfigPayload) deploy(env *model.Environment, c *model.Config, payloadBytes []byte) (err error) {
+	client := http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+	}
+	url, err := env.GetUrl("/api/config")
+	if err != nil {
+		return
+	}
+	req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(payloadBytes))
+	if err != nil {
+		return
+	}
+	req.Header.Set("X-Node-Secret", env.Token)
+	resp, err := client.Do(req)
+	if err != nil {
+		return
+	}
+	defer resp.Body.Close()
+
+	respBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return
+	}
+
+	notificationPayload := &SyncNotificationPayload{
+		StatusCode: resp.StatusCode,
+		ConfigName: c.Name,
+		EnvName:    env.Name,
+		RespBody:   string(respBody),
+	}
+
+	notificationPayloadBytes, err := json.Marshal(notificationPayload)
+	if err != nil {
+		return
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		notification.Error("Sync Config Error", string(notificationPayloadBytes))
+		return
+	}
+
+	notification.Success("Sync Config Success", string(notificationPayloadBytes))
+
+	// handle rename
+	if p.NewFilepath == "" || p.Filepath == p.NewFilepath {
+		return
+	}
+
+	payload := &RenameConfigPayload{
+		Filepath:    p.Filepath,
+		NewFilepath: p.NewFilepath,
+	}
+
+	err = payload.rename(env)
+
+	return
+}
+
+type RenameConfigPayload struct {
+	Filepath    string `json:"filepath"`
+	NewFilepath string `json:"new_filepath"`
+}
+
+type SyncRenameNotificationPayload struct {
+	StatusCode int    `json:"status_code"`
+	OrigPath   string `json:"orig_path"`
+	NewPath    string `json:"new_path"`
+	EnvName    string `json:"env_name"`
+	RespBody   string `json:"resp_body"`
+}
+
+func (p *RenameConfigPayload) rename(env *model.Environment) (err error) {
+	// handle rename
+	if p.NewFilepath == "" || p.Filepath == p.NewFilepath {
+		return
+	}
+
+	client := http.Client{
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		},
+	}
+
+	payloadBytes, err := json.Marshal(gin.H{
+		"base_path": strings.ReplaceAll(filepath.Dir(p.Filepath), nginx.GetConfPath(), ""),
+		"orig_name": filepath.Base(p.Filepath),
+		"new_name":  filepath.Base(p.NewFilepath),
+	})
+	if err != nil {
+		return
+	}
+	url, err := env.GetUrl("/api/config_rename")
+	if err != nil {
+		return
+	}
+	req, err := http.NewRequest(http.MethodPost, url, bytes.NewBuffer(payloadBytes))
+	if err != nil {
+		return
+	}
+	req.Header.Set("X-Node-Secret", env.Token)
+	resp, err := client.Do(req)
+	if err != nil {
+		return
+	}
+	defer resp.Body.Close()
+
+	respBody, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return
+	}
+
+	notificationPayload := &SyncRenameNotificationPayload{
+		StatusCode: resp.StatusCode,
+		OrigPath:   p.Filepath,
+		NewPath:    p.NewFilepath,
+		EnvName:    env.Name,
+		RespBody:   string(respBody),
+	}
+
+	notificationPayloadBytes, err := json.Marshal(notificationPayload)
+	if err != nil {
+		return
+	}
+
+	if resp.StatusCode != http.StatusOK {
+		notification.Error("Rename Remote Config Error", string(notificationPayloadBytes))
+		return
+	}
+
+	notification.Success("Rename Remote Config Success", string(notificationPayloadBytes))
+
+	return
+}

+ 2 - 0
internal/helper/directory_test.go

@@ -8,4 +8,6 @@ import (
 func TestIsUnderDirectory(t *testing.T) {
 	assert.Equal(t, true, IsUnderDirectory("/etc/nginx/nginx.conf", "/etc/nginx"))
 	assert.Equal(t, false, IsUnderDirectory("../../root/nginx.conf", "/etc/nginx"))
+	assert.Equal(t, false, IsUnderDirectory("/etc/nginx/../../root/nginx.conf", "/etc/nginx"))
+	assert.Equal(t, false, IsUnderDirectory("/etc/nginx/../../etc/nginx/../../root/nginx.conf", "/etc/nginx"))
 }

+ 2 - 2
router/ip.go → internal/middleware/ip_whitelist.go

@@ -1,4 +1,4 @@
-package router
+package middleware
 
 import (
 	"github.com/0xJacky/Nginx-UI/settings"
@@ -7,7 +7,7 @@ import (
 	"net/http"
 )
 
-func ipWhiteList() gin.HandlerFunc {
+func IPWhiteList() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		clientIP := c.ClientIP()
 		if len(settings.AuthSettings.IPWhiteList) == 0 || clientIP == "127.0.0.1" {

+ 8 - 44
router/middleware.go → internal/middleware/middleware.go

@@ -1,11 +1,10 @@
-package router
+package middleware
 
 import (
 	"encoding/base64"
 	"github.com/0xJacky/Nginx-UI/app"
 	"github.com/0xJacky/Nginx-UI/internal/logger"
 	"github.com/0xJacky/Nginx-UI/internal/user"
-	"github.com/0xJacky/Nginx-UI/model"
 	"github.com/0xJacky/Nginx-UI/settings"
 	"github.com/gin-contrib/static"
 	"github.com/gin-gonic/gin"
@@ -16,7 +15,7 @@ import (
 	"strings"
 )
 
-func recovery() gin.HandlerFunc {
+func Recovery() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		defer func() {
 			if err := recover(); err != nil {
@@ -34,7 +33,7 @@ func recovery() gin.HandlerFunc {
 	}
 }
 
-func authRequired() gin.HandlerFunc {
+func AuthRequired() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		abortWithAuthFailure := func() {
 			c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
@@ -75,46 +74,11 @@ func authRequired() gin.HandlerFunc {
 	}
 }
 
-func required2FA() gin.HandlerFunc {
-	return func(c *gin.Context) {
-		u, ok := c.Get("user")
-		if !ok {
-			c.Next()
-			return
-		}
-		cUser := u.(*model.Auth)
-		if !cUser.EnabledOTP() {
-			c.Next()
-			return
-		}
-		ssid := c.GetHeader("X-Secure-Session-ID")
-		if ssid == "" {
-			ssid = c.Query("X-Secure-Session-ID")
-		}
-		if ssid == "" {
-			c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
-				"message": "Secure Session ID is empty",
-			})
-			return
-		}
-
-		if user.VerifySecureSessionID(ssid, cUser.ID) {
-			c.Next()
-			return
-		}
-
-		c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
-			"message": "Secure Session ID is invalid",
-		})
-		return
-	}
-}
-
-type serverFileSystemType struct {
+type ServerFileSystemType struct {
 	http.FileSystem
 }
 
-func (f serverFileSystemType) Exists(prefix string, _path string) bool {
+func (f ServerFileSystemType) Exists(prefix string, _path string) bool {
 	file, err := f.Open(path.Join(prefix, _path))
 	if file != nil {
 		defer func(file http.File) {
@@ -127,7 +91,7 @@ func (f serverFileSystemType) Exists(prefix string, _path string) bool {
 	return err == nil
 }
 
-func mustFS(dir string) (serverFileSystem static.ServeFileSystem) {
+func MustFs(dir string) (serverFileSystem static.ServeFileSystem) {
 
 	sub, err := fs.Sub(app.DistFS, path.Join("dist", dir))
 
@@ -136,14 +100,14 @@ func mustFS(dir string) (serverFileSystem static.ServeFileSystem) {
 		return
 	}
 
-	serverFileSystem = serverFileSystemType{
+	serverFileSystem = ServerFileSystemType{
 		http.FS(sub),
 	}
 
 	return
 }
 
-func cacheJs() gin.HandlerFunc {
+func CacheJs() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		if strings.Contains(c.Request.URL.String(), "js") {
 			c.Header("Cache-Control", "max-age: 1296000")

+ 2 - 2
router/proxy.go → internal/middleware/proxy.go

@@ -1,4 +1,4 @@
-package router
+package middleware
 
 import (
 	"crypto/tls"
@@ -11,7 +11,7 @@ import (
 	"net/url"
 )
 
-func proxy() gin.HandlerFunc {
+func Proxy() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		nodeID, ok := c.Get("ProxyNodeID")
 		if !ok {

+ 2 - 2
router/proxy_ws.go → internal/middleware/proxy_ws.go

@@ -1,4 +1,4 @@
-package router
+package middleware
 
 import (
 	"github.com/0xJacky/Nginx-UI/internal/logger"
@@ -9,7 +9,7 @@ import (
 	"net/http"
 )
 
-func proxyWs() gin.HandlerFunc {
+func ProxyWs() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		nodeID, ok := c.Get("ProxyNodeID")
 		if !ok {

+ 43 - 0
internal/middleware/secure_session.go

@@ -0,0 +1,43 @@
+package middleware
+
+import (
+	"github.com/0xJacky/Nginx-UI/internal/user"
+	"github.com/0xJacky/Nginx-UI/model"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func RequireSecureSession() gin.HandlerFunc {
+	return func(c *gin.Context) {
+		u, ok := c.Get("user")
+		if !ok {
+			c.Next()
+			return
+		}
+		cUser := u.(*model.Auth)
+		if !cUser.EnabledOTP() {
+			c.Next()
+			return
+		}
+		ssid := c.GetHeader("X-Secure-Session-ID")
+		if ssid == "" {
+			ssid = c.Query("X-Secure-Session-ID")
+		}
+		if ssid == "" {
+			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
+				"message": "Secure Session ID is empty",
+			})
+			return
+		}
+
+		if user.VerifySecureSessionID(ssid, cUser.ID) {
+			c.Next()
+			return
+		}
+
+		c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
+			"message": "Secure Session ID is invalid",
+		})
+		return
+	}
+}

+ 9 - 0
model/config.go

@@ -0,0 +1,9 @@
+package model
+
+type Config struct {
+	Model
+	Name        string `json:"name"`
+	Filepath    string `json:"filepath"`
+	SyncNodeIds []int  `json:"sync_node_ids" gorm:"serializer:json"`
+    SyncOverwrite  bool `json:"sync_overwrite"`
+}

+ 1 - 0
model/model.go

@@ -36,6 +36,7 @@ func GenerateAllModel() []any {
 		Notification{},
 		AcmeUser{},
 		BanIP{},
+		Config{},
 	}
 }
 

+ 28 - 20
query/certs.gen.go

@@ -45,6 +45,8 @@ func newCert(db *gorm.DB, opts ...gen.DOOption) cert {
 	_cert.Log = field.NewString(tableName, "log")
 	_cert.Resource = field.NewField(tableName, "resource")
 	_cert.SyncNodeIds = field.NewField(tableName, "sync_node_ids")
+	_cert.MustStaple = field.NewBool(tableName, "must_staple")
+	_cert.LegoDisableCNAMESupport = field.NewBool(tableName, "lego_disable_cname_support")
 	_cert.DnsCredential = certBelongsToDnsCredential{
 		db: db.Session(&gorm.Session{}),
 
@@ -65,25 +67,27 @@ func newCert(db *gorm.DB, opts ...gen.DOOption) cert {
 type cert struct {
 	certDo
 
-	ALL                   field.Asterisk
-	ID                    field.Int
-	CreatedAt             field.Time
-	UpdatedAt             field.Time
-	DeletedAt             field.Field
-	Name                  field.String
-	Domains               field.Field
-	Filename              field.String
-	SSLCertificatePath    field.String
-	SSLCertificateKeyPath field.String
-	AutoCert              field.Int
-	ChallengeMethod       field.String
-	DnsCredentialID       field.Int
-	ACMEUserID            field.Int
-	KeyType               field.String
-	Log                   field.String
-	Resource              field.Field
-	SyncNodeIds           field.Field
-	DnsCredential         certBelongsToDnsCredential
+	ALL                     field.Asterisk
+	ID                      field.Int
+	CreatedAt               field.Time
+	UpdatedAt               field.Time
+	DeletedAt               field.Field
+	Name                    field.String
+	Domains                 field.Field
+	Filename                field.String
+	SSLCertificatePath      field.String
+	SSLCertificateKeyPath   field.String
+	AutoCert                field.Int
+	ChallengeMethod         field.String
+	DnsCredentialID         field.Int
+	ACMEUserID              field.Int
+	KeyType                 field.String
+	Log                     field.String
+	Resource                field.Field
+	SyncNodeIds             field.Field
+	MustStaple              field.Bool
+	LegoDisableCNAMESupport field.Bool
+	DnsCredential           certBelongsToDnsCredential
 
 	ACMEUser certBelongsToACMEUser
 
@@ -119,6 +123,8 @@ func (c *cert) updateTableName(table string) *cert {
 	c.Log = field.NewString(table, "log")
 	c.Resource = field.NewField(table, "resource")
 	c.SyncNodeIds = field.NewField(table, "sync_node_ids")
+	c.MustStaple = field.NewBool(table, "must_staple")
+	c.LegoDisableCNAMESupport = field.NewBool(table, "lego_disable_cname_support")
 
 	c.fillFieldMap()
 
@@ -135,7 +141,7 @@ func (c *cert) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
 }
 
 func (c *cert) fillFieldMap() {
-	c.fieldMap = make(map[string]field.Expr, 19)
+	c.fieldMap = make(map[string]field.Expr, 21)
 	c.fieldMap["id"] = c.ID
 	c.fieldMap["created_at"] = c.CreatedAt
 	c.fieldMap["updated_at"] = c.UpdatedAt
@@ -153,6 +159,8 @@ func (c *cert) fillFieldMap() {
 	c.fieldMap["log"] = c.Log
 	c.fieldMap["resource"] = c.Resource
 	c.fieldMap["sync_node_ids"] = c.SyncNodeIds
+	c.fieldMap["must_staple"] = c.MustStaple
+	c.fieldMap["lego_disable_cname_support"] = c.LegoDisableCNAMESupport
 
 }
 

+ 370 - 0
query/configs.gen.go

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

+ 8 - 0
query/gen.go

@@ -23,6 +23,7 @@ var (
 	BanIP         *banIP
 	Cert          *cert
 	ChatGPTLog    *chatGPTLog
+	Config        *config
 	ConfigBackup  *configBackup
 	DnsCredential *dnsCredential
 	Environment   *environment
@@ -39,6 +40,7 @@ func SetDefault(db *gorm.DB, opts ...gen.DOOption) {
 	BanIP = &Q.BanIP
 	Cert = &Q.Cert
 	ChatGPTLog = &Q.ChatGPTLog
+	Config = &Q.Config
 	ConfigBackup = &Q.ConfigBackup
 	DnsCredential = &Q.DnsCredential
 	Environment = &Q.Environment
@@ -56,6 +58,7 @@ func Use(db *gorm.DB, opts ...gen.DOOption) *Query {
 		BanIP:         newBanIP(db, opts...),
 		Cert:          newCert(db, opts...),
 		ChatGPTLog:    newChatGPTLog(db, opts...),
+		Config:        newConfig(db, opts...),
 		ConfigBackup:  newConfigBackup(db, opts...),
 		DnsCredential: newDnsCredential(db, opts...),
 		Environment:   newEnvironment(db, opts...),
@@ -74,6 +77,7 @@ type Query struct {
 	BanIP         banIP
 	Cert          cert
 	ChatGPTLog    chatGPTLog
+	Config        config
 	ConfigBackup  configBackup
 	DnsCredential dnsCredential
 	Environment   environment
@@ -93,6 +97,7 @@ func (q *Query) clone(db *gorm.DB) *Query {
 		BanIP:         q.BanIP.clone(db),
 		Cert:          q.Cert.clone(db),
 		ChatGPTLog:    q.ChatGPTLog.clone(db),
+		Config:        q.Config.clone(db),
 		ConfigBackup:  q.ConfigBackup.clone(db),
 		DnsCredential: q.DnsCredential.clone(db),
 		Environment:   q.Environment.clone(db),
@@ -119,6 +124,7 @@ func (q *Query) ReplaceDB(db *gorm.DB) *Query {
 		BanIP:         q.BanIP.replaceDB(db),
 		Cert:          q.Cert.replaceDB(db),
 		ChatGPTLog:    q.ChatGPTLog.replaceDB(db),
+		Config:        q.Config.replaceDB(db),
 		ConfigBackup:  q.ConfigBackup.replaceDB(db),
 		DnsCredential: q.DnsCredential.replaceDB(db),
 		Environment:   q.Environment.replaceDB(db),
@@ -135,6 +141,7 @@ type queryCtx struct {
 	BanIP         *banIPDo
 	Cert          *certDo
 	ChatGPTLog    *chatGPTLogDo
+	Config        *configDo
 	ConfigBackup  *configBackupDo
 	DnsCredential *dnsCredentialDo
 	Environment   *environmentDo
@@ -151,6 +158,7 @@ func (q *Query) WithContext(ctx context.Context) *queryCtx {
 		BanIP:         q.BanIP.WithContext(ctx),
 		Cert:          q.Cert.WithContext(ctx),
 		ChatGPTLog:    q.ChatGPTLog.WithContext(ctx),
+		Config:        q.Config.WithContext(ctx),
 		ConfigBackup:  q.ConfigBackup.WithContext(ctx),
 		DnsCredential: q.DnsCredential.WithContext(ctx),
 		Environment:   q.Environment.WithContext(ctx),

+ 0 - 154
router/operation_sync.go

@@ -1,154 +0,0 @@
-package router
-
-import (
-	"bytes"
-	"crypto/tls"
-	"encoding/json"
-	"fmt"
-	"github.com/0xJacky/Nginx-UI/internal/analytic"
-	"github.com/0xJacky/Nginx-UI/internal/logger"
-	"github.com/gin-gonic/gin"
-	"github.com/pkg/errors"
-	"io"
-	"net/http"
-	"net/url"
-	"regexp"
-	"sync"
-)
-
-type ErrorRes struct {
-	Message string `json:"message"`
-}
-
-type toolBodyWriter struct {
-	gin.ResponseWriter
-	body *bytes.Buffer
-}
-
-func (r toolBodyWriter) Write(b []byte) (int, error) {
-	return r.body.Write(b)
-}
-
-// OperationSync 针对配置了vip的环境操作进行同步
-func OperationSync() gin.HandlerFunc {
-	return func(c *gin.Context) {
-		bodyBytes, _ := PeekRequest(c.Request)
-		wb := &toolBodyWriter{
-			body:           &bytes.Buffer{},
-			ResponseWriter: c.Writer,
-		}
-		c.Writer = wb
-
-		c.Next()
-		if c.Request.Method == "GET" || !statusValid(c.Writer.Status()) { // 请求有问题,无需执行同步操作
-			return
-		}
-
-		totalCount := 0
-		successCount := 0
-		detailMsg := ""
-		// 后置处理操作同步
-		wg := sync.WaitGroup{}
-		for _, node := range analytic.NodeMap {
-			wg.Add(1)
-			node := node
-			go func(data analytic.Node) {
-				defer wg.Done()
-				if node.OperationSync && node.Status && requestUrlMatch(c.Request.URL.Path, data) { // 开启操作同步且当前状态正常
-					totalCount++
-					if err := syncNodeOperation(c, data, bodyBytes); err != nil {
-						detailMsg += fmt.Sprintf("node_name: %s, err_msg: %s; ", data.Name, err)
-						return
-					}
-					successCount++
-				}
-			}(*node)
-		}
-		wg.Wait()
-		if successCount < totalCount { // 如果有错误,替换原来的消息内容
-			originBytes := wb.body
-			logger.Infof("origin response body: %s", originBytes)
-			// clear Origin Buffer
-			wb.body = &bytes.Buffer{}
-			wb.ResponseWriter.WriteHeader(http.StatusInternalServerError)
-
-			errorRes := ErrorRes{
-				Message: fmt.Sprintf("operation sync failed, total: %d, success: %d, fail: %d, detail: %s", totalCount, successCount, totalCount-successCount, detailMsg),
-			}
-			byts, _ := json.Marshal(errorRes)
-			_, err := wb.Write(byts)
-
-			if err != nil {
-				logger.Error(err)
-			}
-		}
-		_, err := wb.ResponseWriter.Write(wb.body.Bytes())
-		if err != nil {
-			logger.Error(err)
-		}
-	}
-}
-
-func PeekRequest(request *http.Request) ([]byte, error) {
-	if request.Body != nil {
-		byts, err := io.ReadAll(request.Body) // io.ReadAll as Go 1.16, below please use ioutil.ReadAll
-		if err != nil {
-			return nil, err
-		}
-		request.Body = io.NopCloser(bytes.NewReader(byts))
-		return byts, nil
-	}
-	return make([]byte, 0), nil
-}
-
-func requestUrlMatch(url string, node analytic.Node) bool {
-	p, _ := regexp.Compile(node.SyncApiRegex)
-	result := p.FindAllString(url, -1)
-	if len(result) > 0 && result[0] == url {
-		return true
-	}
-	return false
-}
-
-func statusValid(code int) bool {
-	return code < http.StatusMultipleChoices
-}
-
-func syncNodeOperation(c *gin.Context, node analytic.Node, bodyBytes []byte) error {
-	u, err := url.JoinPath(node.URL, c.Request.RequestURI)
-	if err != nil {
-		return err
-	}
-	decodedUri, err := url.QueryUnescape(u)
-	if err != nil {
-		return err
-	}
-	logger.Debugf("syncNodeOperation request: %s, node_id: %d, node_name: %s", decodedUri, node.ID, node.Name)
-	client := http.Client{
-		Transport: &http.Transport{
-			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-		},
-	}
-
-	req, err := http.NewRequest(c.Request.Method, decodedUri, bytes.NewReader(bodyBytes))
-	req.Header.Set("X-Node-Secret", node.Token)
-
-	res, err := client.Do(req)
-	if err != nil {
-		return err
-	}
-	defer res.Body.Close()
-	byts, err := io.ReadAll(res.Body)
-	if err != nil {
-		return err
-	}
-	if !statusValid(res.StatusCode) {
-		errRes := ErrorRes{}
-		if err = json.Unmarshal(byts, &errRes); err != nil {
-			return err
-		}
-		return errors.New(errRes.Message)
-	}
-	logger.Debug("syncNodeOperation result: ", string(byts))
-	return nil
-}

+ 71 - 71
router/routers.go

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