feat: deploy config to remote nodes #359

This commit is contained in:
Jacky 2024-07-26 13:53:38 +08:00
parent e75dce92ad
commit 1c1da92363
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
46 changed files with 1480 additions and 605 deletions

View file

@ -5,10 +5,13 @@ import (
"github.com/0xJacky/Nginx-UI/internal/config" "github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/helper" "github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx" "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/gin-gonic/gin"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
"net/http" "net/http"
"os" "os"
"path/filepath"
"time" "time"
) )
@ -18,6 +21,7 @@ func AddConfig(c *gin.Context) {
NewFilepath string `json:"new_filepath" binding:"required"` NewFilepath string `json:"new_filepath" binding:"required"`
Content string `json:"content"` Content string `json:"content"`
Overwrite bool `json:"overwrite"` Overwrite bool `json:"overwrite"`
SyncNodeIds []int `json:"sync_node_ids"`
} }
if !api.BindAndValid(c, &json) { if !api.BindAndValid(c, &json) {
@ -41,6 +45,16 @@ func AddConfig(c *gin.Context) {
return 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) err := os.WriteFile(path, []byte(content), 0644)
if err != nil { if err != nil {
api.ErrHandler(c, err) api.ErrHandler(c, err)
@ -55,6 +69,24 @@ func AddConfig(c *gin.Context) {
return 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{ c.JSON(http.StatusOK, config.Config{
Name: name, Name: name,
Content: content, Content: content,

View file

@ -12,6 +12,12 @@ import (
"os" "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) { func GetConfig(c *gin.Context) {
name := c.Param("name") name := c.Param("name")
@ -34,7 +40,7 @@ func GetConfig(c *gin.Context) {
api.ErrHandler(c, err) api.ErrHandler(c, err)
return return
} }
q := query.Config
g := query.ChatGPTLog g := query.ChatGPTLog
chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate() chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
if err != nil { if err != nil {
@ -46,11 +52,21 @@ func GetConfig(c *gin.Context) {
chatgpt.Content = make([]openai.ChatCompletionMessage, 0) chatgpt.Content = make([]openai.ChatCompletionMessage, 0)
} }
c.JSON(http.StatusOK, config.Config{ 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(), Name: stat.Name(),
Content: string(content), Content: string(content),
ChatGPTMessages: chatgpt.Content, ChatGPTMessages: chatgpt.Content,
FilePath: path, FilePath: path,
ModifiedAt: stat.ModTime(), ModifiedAt: stat.ModTime(),
},
SyncNodeIds: cfg.SyncNodeIds,
SyncOverwrite: cfg.SyncOverwrite,
}) })
} }

View file

@ -30,6 +30,7 @@ func Mkdir(c *gin.Context) {
api.ErrHandler(c, err) api.ErrHandler(c, err)
return return
} }
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": "ok", "message": "ok",
}) })

View file

@ -5,6 +5,7 @@ import (
"github.com/0xJacky/Nginx-UI/internal/config" "github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/helper" "github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx" "github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai" "github.com/sashabaranov/go-openai"
@ -24,6 +25,8 @@ func EditConfig(c *gin.Context) {
Filepath string `json:"filepath" binding:"required"` Filepath string `json:"filepath" binding:"required"`
NewFilepath string `json:"new_filepath" binding:"required"` NewFilepath string `json:"new_filepath" binding:"required"`
Content string `json:"content"` Content string `json:"content"`
Overwrite bool `json:"overwrite"`
SyncNodeIds []int `json:"sync_node_ids"`
} }
if !api.BindAndValid(c, &json) { if !api.BindAndValid(c, &json) {
return return
@ -66,8 +69,25 @@ func EditConfig(c *gin.Context) {
} }
} }
g := query.ChatGPTLog 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 // handle rename
if path != json.NewFilepath { if path != json.NewFilepath {
if helper.FileExists(json.NewFilepath) { if helper.FileExists(json.NewFilepath) {
@ -87,6 +107,12 @@ func EditConfig(c *gin.Context) {
_, _ = g.Where(g.Name.Eq(path)).Update(g.Name, json.NewFilepath) _, _ = 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() output := nginx.Reload()
if nginx.GetLogLevel(output) >= nginx.Warn { if nginx.GetLogLevel(output) >= nginx.Warn {
c.JSON(http.StatusInternalServerError, gin.H{ c.JSON(http.StatusInternalServerError, gin.H{

View file

@ -2,7 +2,9 @@ package config
import ( import (
"github.com/0xJacky/Nginx-UI/api" "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/helper"
"github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/internal/nginx" "github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/query" "github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -15,11 +17,13 @@ func Rename(c *gin.Context) {
BasePath string `json:"base_path"` BasePath string `json:"base_path"`
OrigName string `json:"orig_name"` OrigName string `json:"orig_name"`
NewName string `json:"new_name"` NewName string `json:"new_name"`
SyncNodeIds []int `json:"sync_node_ids" gorm:"serializer:json"`
} }
if !api.BindAndValid(c, &json) { if !api.BindAndValid(c, &json) {
return return
} }
if json.OrigName == json.OrigName { logger.Debug(json)
if json.OrigName == json.NewName {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"message": "ok", "message": "ok",
}) })
@ -55,11 +59,36 @@ func Rename(c *gin.Context) {
return return
} }
if !stat.IsDir() {
// update ChatGPT records // update ChatGPT records
g := query.ChatGPTLog 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(newFullPath)).Delete()
_, _ = g.Where(g.Name.Eq(origFullPath)).Update(g.Name, newFullPath) _, _ = 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{ c.JSON(http.StatusOK, gin.H{

View file

@ -1,6 +1,9 @@
package config 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) { func InitRouter(r *gin.RouterGroup) {
r.GET("config_base_path", GetBasePath) r.GET("config_base_path", GetBasePath)
@ -9,6 +12,10 @@ func InitRouter(r *gin.RouterGroup) {
r.GET("config/*name", GetConfig) r.GET("config/*name", GetConfig)
r.POST("config", AddConfig) r.POST("config", AddConfig)
r.POST("config/*name", EditConfig) r.POST("config/*name", EditConfig)
r.POST("config_mkdir", Mkdir)
r.POST("config_rename", Rename) o := r.Group("", middleware.RequireSecureSession())
{
o.POST("config_mkdir", Mkdir)
o.POST("config_rename", Rename)
}
} }

View file

@ -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) { func StartSecure2FASession(c *gin.Context) {
var json struct { var json struct {
OTP string `json:"otp"` OTP string `json:"otp"`

View file

@ -1,6 +1,9 @@
package user 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) { func InitAuthRouter(r *gin.RouterGroup) {
r.POST("/login", Login) r.POST("/login", Login)
@ -23,5 +26,8 @@ func InitUserRouter(r *gin.RouterGroup) {
r.GET("/otp_secret", GenerateTOTP) r.GET("/otp_secret", GenerateTOTP)
r.POST("/otp_enroll", EnrollTOTP) r.POST("/otp_enroll", EnrollTOTP)
r.POST("/otp_reset", ResetOTP) r.POST("/otp_reset", ResetOTP)
r.GET("/otp_secure_session_status",
middleware.RequireSecureSession(), SecureSessionStatus)
r.POST("/otp_secure_session", StartSecure2FASession) r.POST("/otp_secure_session", StartSecure2FASession)
} }

View file

@ -8,6 +8,8 @@ export interface Config {
chatgpt_messages: ChatComplicationMessage[] chatgpt_messages: ChatComplicationMessage[]
filepath: string filepath: string
modified_at: string modified_at: string
sync_node_ids?: number[]
sync_overwrite?: false
} }
class ConfigCurd extends Curd<Config> { class ConfigCurd extends Curd<Config> {
@ -23,8 +25,13 @@ class ConfigCurd extends Curd<Config> {
return http.post('/config_mkdir', { base_path: basePath, folder_name: name }) return http.post('/config_mkdir', { base_path: basePath, folder_name: name })
} }
rename(basePath: string, origName: string, newName: string) { rename(basePath: string, origName: string, newName: string, syncNodeIds: number[]) {
return http.post('/config_rename', { base_path: basePath, orig_name: origName, new_name: newName }) return http.post('/config_rename', {
base_path: basePath,
orig_name: origName,
new_name: newName,
sync_node_ids: syncNodeIds,
})
} }
} }

View file

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

View file

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

View file

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

View file

@ -1,4 +1,11 @@
import type { customRender } from '@/components/StdDesign/StdDataDisplay/StdTableTransformer' 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) => { export const detailRender = (args: customRender) => {
switch (args.record.title) { switch (args.record.title) {
@ -6,26 +13,15 @@ export const detailRender = (args: customRender) => {
return syncCertificateSuccess(args.text) return syncCertificateSuccess(args.text)
case 'Sync Certificate Error': case 'Sync Certificate Error':
return syncCertificateError(args.text) return syncCertificateError(args.text)
case 'Sync Rename Configuration Success':
return syncRenameConfigSuccess(args.text)
case 'Sync Rename Configuration Error':
return syncRenameConfigError(args.text)
case 'Sync Configuration Success':
return syncConfigSuccess(args.text)
case 'Sync Configuration Error':
return syncConfigError(args.text)
default: default:
return args.text 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)
}

View file

@ -5,11 +5,6 @@ import OTPAuthorization from '@/components/OTP/OTPAuthorization.vue'
import otp from '@/api/otp' import otp from '@/api/otp'
import { useUserStore } from '@/pinia' import { useUserStore } from '@/pinia'
export interface OTPModalProps {
onOk?: (secureSessionId: string) => void
onCancel?: () => void
}
const useOTPModal = () => { const useOTPModal = () => {
const refOTPAuthorization = ref<typeof OTPAuthorization>() const refOTPAuthorization = ref<typeof OTPAuthorization>()
const randomId = Math.random().toString(36).substring(2, 8) const randomId = Math.random().toString(36).substring(2, 8)
@ -26,10 +21,12 @@ const useOTPModal = () => {
document.head.appendChild(style) document.head.appendChild(style)
} }
const open = async ({ onOk, onCancel }: OTPModalProps) => { const open = async (): Promise<string> => {
const { status } = await otp.status() const { status } = await otp.status()
return new Promise((resolve, reject) => {
if (!status) { if (!status) {
onOk?.('') resolve('')
return return
} }
@ -37,7 +34,7 @@ const useOTPModal = () => {
const cookies = useCookies(['nginx-ui-2fa']) const cookies = useCookies(['nginx-ui-2fa'])
const ssid = cookies.get('secure_session_id') const ssid = cookies.get('secure_session_id')
if (ssid) { if (ssid) {
onOk?.(ssid) resolve(ssid)
secureSessionId.value = ssid secureSessionId.value = ssid
return return
@ -56,7 +53,7 @@ const useOTPModal = () => {
const verify = (passcode: string, recovery: string) => { const verify = (passcode: string, recovery: string) => {
otp.start_secure_session(passcode, recovery).then(r => { otp.start_secure_session(passcode, recovery).then(r => {
cookies.set('secure_session_id', r.session_id, { maxAge: 60 * 3 }) cookies.set('secure_session_id', r.session_id, { maxAge: 60 * 3 })
onOk?.(r.session_id) resolve(r.session_id)
close() close()
secureSessionId.value = r.session_id secureSessionId.value = r.session_id
}).catch(async () => { }).catch(async () => {
@ -74,7 +71,8 @@ const useOTPModal = () => {
footer: false, footer: false,
onCancel: () => { onCancel: () => {
close() close()
onCancel?.() // eslint-disable-next-line prefer-promise-reject-errors
reject()
}, },
}, { }, {
default: () => h( default: () => h(
@ -87,7 +85,8 @@ const useOTPModal = () => {
), ),
}) })
render(vnode, container) render(vnode, container!)
})
} }
return { open } return { open }

View file

@ -194,9 +194,9 @@ msgid "Auto-renewal enabled for %{name}"
msgstr "Auto-renewal enabled for %{name}" msgstr "Auto-renewal enabled for %{name}"
#: src/views/certificate/CertificateEditor.vue:247 #: src/views/certificate/CertificateEditor.vue:247
#: src/views/config/Config.vue:143 src/views/config/ConfigEditor.vue:196 #: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168 #: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
#: src/views/stream/StreamEdit.vue:245 #: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
msgid "Back" msgid "Back"
msgstr "Back" msgstr "Back"
@ -369,7 +369,7 @@ msgstr ""
msgid "Configuration Name" msgid "Configuration Name"
msgstr "Configuration Name" msgstr "Configuration Name"
#: src/views/config/Config.vue:91 #: src/views/config/ConfigList.vue:91
msgid "Configurations" msgid "Configurations"
msgstr "Configurations" msgstr "Configurations"
@ -420,12 +420,12 @@ msgstr "Created at"
msgid "Create Another" msgid "Create Another"
msgstr "Create Another" msgstr "Create Another"
#: src/views/config/Config.vue:99 #: src/views/config/ConfigList.vue:109
#, fuzzy #, fuzzy
msgid "Create File" msgid "Create File"
msgstr "Created at" msgstr "Created at"
#: src/views/config/components/Mkdir.vue:50 src/views/config/Config.vue:100 #: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
#, fuzzy #, fuzzy
msgid "Create Folder" msgid "Create Folder"
msgstr "Create Another" msgstr "Create Another"
@ -474,8 +474,8 @@ msgid ""
"indicator." "indicator."
msgstr "" msgstr ""
#: src/routes/index.ts:39 src/views/config/Config.vue:57 #: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
#: src/views/config/ConfigEditor.vue:118 src/views/config/ConfigEditor.vue:79 #: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
msgid "Dashboard" msgid "Dashboard"
msgstr "Dashboard" msgstr "Dashboard"
@ -803,6 +803,10 @@ msgstr "Enabled successfully"
msgid "Encrypt website with Let's Encrypt" msgid "Encrypt website with Let's Encrypt"
msgstr "Encrypt website with Let's Encrypt" msgstr "Encrypt website with Let's Encrypt"
#: src/views/config/ConfigList.vue:151
msgid "Enter"
msgstr ""
#: src/routes/index.ts:228 src/views/environment/Environment.vue:34 #: src/routes/index.ts:228 src/views/environment/Environment.vue:34
msgid "Environment" msgid "Environment"
msgstr "" msgstr ""
@ -1191,8 +1195,8 @@ msgstr ""
"Make sure you have configured a reverse proxy for .well-known directory to " "Make sure you have configured a reverse proxy for .well-known directory to "
"HTTPChallengePort (default: 9180) before getting the certificate." "HTTPChallengePort (default: 9180) before getting the certificate."
#: src/routes/index.ts:102 src/views/config/Config.vue:62 #: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
#: src/views/config/ConfigEditor.vue:123 src/views/config/ConfigEditor.vue:84 #: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
msgid "Manage Configs" msgid "Manage Configs"
msgstr "Manage Configs" msgstr "Manage Configs"
@ -1239,6 +1243,7 @@ msgstr "Advance Mode"
#: src/components/ChatGPT/ChatGPT.vue:248 #: src/components/ChatGPT/ChatGPT.vue:248
#: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
#: src/components/StdDesign/StdDataDisplay/StdTable.vue:532 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
#: src/views/config/ConfigList.vue:151
#, fuzzy #, fuzzy
msgid "Modify" msgid "Modify"
msgstr "Modify Config" msgstr "Modify Config"
@ -1703,7 +1708,8 @@ msgstr "Saved successfully"
msgid "Removed successfully" msgid "Removed successfully"
msgstr "Saved successfully" msgstr "Saved successfully"
#: src/views/config/components/Rename.vue:52 src/views/config/Config.vue:130 #: src/views/config/components/Rename.vue:52
#: src/views/config/ConfigList.vue:159
#: src/views/domain/ngx_conf/NgxUpstream.vue:123 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
#, fuzzy #, fuzzy
msgid "Rename" msgid "Rename"

View file

@ -194,9 +194,9 @@ msgid "Auto-renewal enabled for %{name}"
msgstr "Renovación automática habilitada por %{name}" msgstr "Renovación automática habilitada por %{name}"
#: src/views/certificate/CertificateEditor.vue:247 #: src/views/certificate/CertificateEditor.vue:247
#: src/views/config/Config.vue:143 src/views/config/ConfigEditor.vue:196 #: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168 #: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
#: src/views/stream/StreamEdit.vue:245 #: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
msgid "Back" msgid "Back"
msgstr "Volver" msgstr "Volver"
@ -362,7 +362,7 @@ msgstr "El archivo de configuración se probó exitosamente"
msgid "Configuration Name" msgid "Configuration Name"
msgstr "Nombre de la configuración" msgstr "Nombre de la configuración"
#: src/views/config/Config.vue:91 #: src/views/config/ConfigList.vue:91
msgid "Configurations" msgid "Configurations"
msgstr "Configuraciones" msgstr "Configuraciones"
@ -412,12 +412,12 @@ msgstr "Crear"
msgid "Create Another" msgid "Create Another"
msgstr "Crear otro" msgstr "Crear otro"
#: src/views/config/Config.vue:99 #: src/views/config/ConfigList.vue:109
#, fuzzy #, fuzzy
msgid "Create File" msgid "Create File"
msgstr "Crear" msgstr "Crear"
#: src/views/config/components/Mkdir.vue:50 src/views/config/Config.vue:100 #: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
#, fuzzy #, fuzzy
msgid "Create Folder" msgid "Create Folder"
msgstr "Crear otro" msgstr "Crear otro"
@ -466,8 +466,8 @@ msgid ""
"indicator." "indicator."
msgstr "" msgstr ""
#: src/routes/index.ts:39 src/views/config/Config.vue:57 #: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
#: src/views/config/ConfigEditor.vue:118 src/views/config/ConfigEditor.vue:79 #: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
msgid "Dashboard" msgid "Dashboard"
msgstr "Panel" msgstr "Panel"
@ -778,6 +778,10 @@ msgstr "Habilitado con éxito"
msgid "Encrypt website with Let's Encrypt" msgid "Encrypt website with Let's Encrypt"
msgstr "Encriptar sitio web con Let's Encrypt" msgstr "Encriptar sitio web con Let's Encrypt"
#: src/views/config/ConfigList.vue:151
msgid "Enter"
msgstr ""
#: src/routes/index.ts:228 src/views/environment/Environment.vue:34 #: src/routes/index.ts:228 src/views/environment/Environment.vue:34
msgid "Environment" msgid "Environment"
msgstr "Entorno" msgstr "Entorno"
@ -1152,8 +1156,8 @@ msgstr ""
"Asegúrese de haber configurado un proxy reverso para el directorio .well-" "Asegúrese de haber configurado un proxy reverso para el directorio .well-"
"known en HTTPChallengePort antes de obtener el certificado." "known en HTTPChallengePort antes de obtener el certificado."
#: src/routes/index.ts:102 src/views/config/Config.vue:62 #: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
#: src/views/config/ConfigEditor.vue:123 src/views/config/ConfigEditor.vue:84 #: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
msgid "Manage Configs" msgid "Manage Configs"
msgstr "Administrar configuraciones" msgstr "Administrar configuraciones"
@ -1198,6 +1202,7 @@ msgstr "Modo de ejecución"
#: src/components/ChatGPT/ChatGPT.vue:248 #: src/components/ChatGPT/ChatGPT.vue:248
#: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
#: src/components/StdDesign/StdDataDisplay/StdTable.vue:532 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
#: src/views/config/ConfigList.vue:151
msgid "Modify" msgid "Modify"
msgstr "Modificar" msgstr "Modificar"
@ -1660,7 +1665,8 @@ msgstr "Eliminado con éxito"
msgid "Removed successfully" msgid "Removed successfully"
msgstr "Eliminado con éxito" msgstr "Eliminado con éxito"
#: src/views/config/components/Rename.vue:52 src/views/config/Config.vue:130 #: src/views/config/components/Rename.vue:52
#: src/views/config/ConfigList.vue:159
#: src/views/domain/ngx_conf/NgxUpstream.vue:123 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
msgid "Rename" msgid "Rename"
msgstr "Renombrar" msgstr "Renombrar"

View file

@ -197,9 +197,9 @@ msgid "Auto-renewal enabled for %{name}"
msgstr "Renouvellement automatique activé pour %{name}" msgstr "Renouvellement automatique activé pour %{name}"
#: src/views/certificate/CertificateEditor.vue:247 #: src/views/certificate/CertificateEditor.vue:247
#: src/views/config/Config.vue:143 src/views/config/ConfigEditor.vue:196 #: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168 #: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
#: src/views/stream/StreamEdit.vue:245 #: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
msgid "Back" msgid "Back"
msgstr "Retour" msgstr "Retour"
@ -369,7 +369,7 @@ msgstr "Le fichier de configuration est testé avec succès"
msgid "Configuration Name" msgid "Configuration Name"
msgstr "Nom de la configuration" msgstr "Nom de la configuration"
#: src/views/config/Config.vue:91 #: src/views/config/ConfigList.vue:91
msgid "Configurations" msgid "Configurations"
msgstr "Configurations" msgstr "Configurations"
@ -420,12 +420,12 @@ msgstr "Créé le"
msgid "Create Another" msgid "Create Another"
msgstr "Créer un autre" msgstr "Créer un autre"
#: src/views/config/Config.vue:99 #: src/views/config/ConfigList.vue:109
#, fuzzy #, fuzzy
msgid "Create File" msgid "Create File"
msgstr "Créé le" msgstr "Créé le"
#: src/views/config/components/Mkdir.vue:50 src/views/config/Config.vue:100 #: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
#, fuzzy #, fuzzy
msgid "Create Folder" msgid "Create Folder"
msgstr "Créer un autre" msgstr "Créer un autre"
@ -474,8 +474,8 @@ msgid ""
"indicator." "indicator."
msgstr "" msgstr ""
#: src/routes/index.ts:39 src/views/config/Config.vue:57 #: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
#: src/views/config/ConfigEditor.vue:118 src/views/config/ConfigEditor.vue:79 #: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
msgid "Dashboard" msgid "Dashboard"
msgstr "Dashboard" msgstr "Dashboard"
@ -803,6 +803,10 @@ msgstr "Activé avec succès"
msgid "Encrypt website with Let's Encrypt" msgid "Encrypt website with Let's Encrypt"
msgstr "Crypter le site Web avec Let's Encrypt" msgstr "Crypter le site Web avec Let's Encrypt"
#: src/views/config/ConfigList.vue:151
msgid "Enter"
msgstr ""
#: src/routes/index.ts:228 src/views/environment/Environment.vue:34 #: src/routes/index.ts:228 src/views/environment/Environment.vue:34
msgid "Environment" msgid "Environment"
msgstr "" msgstr ""
@ -1193,8 +1197,8 @@ msgstr ""
"Assurez vous d'avoir configuré un reverse proxy pour le répertoire .well-" "Assurez vous d'avoir configuré un reverse proxy pour le répertoire .well-"
"known vers HTTPChallengePort avant d'obtenir le certificat." "known vers HTTPChallengePort avant d'obtenir le certificat."
#: src/routes/index.ts:102 src/views/config/Config.vue:62 #: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
#: src/views/config/ConfigEditor.vue:123 src/views/config/ConfigEditor.vue:84 #: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
msgid "Manage Configs" msgid "Manage Configs"
msgstr "Gérer les configurations" msgstr "Gérer les configurations"
@ -1241,6 +1245,7 @@ msgstr "Mode d'exécution"
#: src/components/ChatGPT/ChatGPT.vue:248 #: src/components/ChatGPT/ChatGPT.vue:248
#: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
#: src/components/StdDesign/StdDataDisplay/StdTable.vue:532 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
#: src/views/config/ConfigList.vue:151
msgid "Modify" msgid "Modify"
msgstr "Modifier" msgstr "Modifier"
@ -1710,7 +1715,8 @@ msgstr "Enregistré avec succès"
msgid "Removed successfully" msgid "Removed successfully"
msgstr "Enregistré avec succès" msgstr "Enregistré avec succès"
#: src/views/config/components/Rename.vue:52 src/views/config/Config.vue:130 #: src/views/config/components/Rename.vue:52
#: src/views/config/ConfigList.vue:159
#: src/views/domain/ngx_conf/NgxUpstream.vue:123 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
#, fuzzy #, fuzzy
msgid "Rename" msgid "Rename"

View file

@ -193,9 +193,9 @@ msgid "Auto-renewal enabled for %{name}"
msgstr "%{name}에 대한 자동 갱신 활성화됨" msgstr "%{name}에 대한 자동 갱신 활성화됨"
#: src/views/certificate/CertificateEditor.vue:247 #: src/views/certificate/CertificateEditor.vue:247
#: src/views/config/Config.vue:143 src/views/config/ConfigEditor.vue:196 #: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168 #: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
#: src/views/stream/StreamEdit.vue:245 #: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
msgid "Back" msgid "Back"
msgstr "뒤로" msgstr "뒤로"
@ -360,7 +360,7 @@ msgstr "구성 파일 테스트 성공"
msgid "Configuration Name" msgid "Configuration Name"
msgstr "구성 이름" msgstr "구성 이름"
#: src/views/config/Config.vue:91 #: src/views/config/ConfigList.vue:91
msgid "Configurations" msgid "Configurations"
msgstr "구성들" msgstr "구성들"
@ -410,12 +410,12 @@ msgstr "생성"
msgid "Create Another" msgid "Create Another"
msgstr "다른 것 생성하기" msgstr "다른 것 생성하기"
#: src/views/config/Config.vue:99 #: src/views/config/ConfigList.vue:109
#, fuzzy #, fuzzy
msgid "Create File" msgid "Create File"
msgstr "생성" msgstr "생성"
#: src/views/config/components/Mkdir.vue:50 src/views/config/Config.vue:100 #: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
#, fuzzy #, fuzzy
msgid "Create Folder" msgid "Create Folder"
msgstr "다른 것 생성하기" msgstr "다른 것 생성하기"
@ -464,8 +464,8 @@ msgid ""
"indicator." "indicator."
msgstr "" msgstr ""
#: src/routes/index.ts:39 src/views/config/Config.vue:57 #: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
#: src/views/config/ConfigEditor.vue:118 src/views/config/ConfigEditor.vue:79 #: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
msgid "Dashboard" msgid "Dashboard"
msgstr "대시보드" msgstr "대시보드"
@ -776,6 +776,11 @@ msgstr "성공적으로 활성화됨"
msgid "Encrypt website with Let's Encrypt" msgid "Encrypt website with Let's Encrypt"
msgstr "Let's Encrypt로 웹사이트 암호화" msgstr "Let's Encrypt로 웹사이트 암호화"
#: src/views/config/ConfigList.vue:151
#, fuzzy
msgid "Enter"
msgstr "간격"
#: src/routes/index.ts:228 src/views/environment/Environment.vue:34 #: src/routes/index.ts:228 src/views/environment/Environment.vue:34
msgid "Environment" msgid "Environment"
msgstr "환경" msgstr "환경"
@ -1170,8 +1175,8 @@ msgstr ""
"인증서를 획득하기 전에 .well-known 디렉토리에 대한역방향 프록시를 " "인증서를 획득하기 전에 .well-known 디렉토리에 대한역방향 프록시를 "
"HTTPChallengePort(기본값: 9180)로 구성했는지 확인하세요." "HTTPChallengePort(기본값: 9180)로 구성했는지 확인하세요."
#: src/routes/index.ts:102 src/views/config/Config.vue:62 #: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
#: src/views/config/ConfigEditor.vue:123 src/views/config/ConfigEditor.vue:84 #: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
msgid "Manage Configs" msgid "Manage Configs"
msgstr "구성 관리" msgstr "구성 관리"
@ -1218,6 +1223,7 @@ msgstr "실행 모드"
#: src/components/ChatGPT/ChatGPT.vue:248 #: src/components/ChatGPT/ChatGPT.vue:248
#: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
#: src/components/StdDesign/StdDataDisplay/StdTable.vue:532 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
#: src/views/config/ConfigList.vue:151
#, fuzzy #, fuzzy
msgid "Modify" msgid "Modify"
msgstr "설정 수정" msgstr "설정 수정"
@ -1686,7 +1692,8 @@ msgstr "성공적으로 제거됨"
msgid "Removed successfully" msgid "Removed successfully"
msgstr "성공적으로 제거됨" msgstr "성공적으로 제거됨"
#: src/views/config/components/Rename.vue:52 src/views/config/Config.vue:130 #: src/views/config/components/Rename.vue:52
#: src/views/config/ConfigList.vue:159
#: src/views/domain/ngx_conf/NgxUpstream.vue:123 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
#, fuzzy #, fuzzy
msgid "Rename" msgid "Rename"

View file

@ -181,8 +181,9 @@ msgid "Auto-renewal enabled for %{name}"
msgstr "" msgstr ""
#: src/views/certificate/CertificateEditor.vue:247 #: src/views/certificate/CertificateEditor.vue:247
#: src/views/config/Config.vue:143
#: src/views/config/ConfigEditor.vue:196 #: 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/domain/DomainEdit.vue:253
#: src/views/nginx_log/NginxLog.vue:168 #: src/views/nginx_log/NginxLog.vue:168
#: src/views/stream/StreamEdit.vue:245 #: src/views/stream/StreamEdit.vue:245
@ -347,7 +348,7 @@ msgstr ""
msgid "Configuration Name" msgid "Configuration Name"
msgstr "" msgstr ""
#: src/views/config/Config.vue:91 #: src/views/config/ConfigList.vue:91
msgid "Configurations" msgid "Configurations"
msgstr "" msgstr ""
@ -397,12 +398,12 @@ msgstr ""
msgid "Create Another" msgid "Create Another"
msgstr "" msgstr ""
#: src/views/config/Config.vue:99 #: src/views/config/ConfigList.vue:109
msgid "Create File" msgid "Create File"
msgstr "" msgstr ""
#: src/views/config/components/Mkdir.vue:50 #: src/views/config/components/Mkdir.vue:50
#: src/views/config/Config.vue:100 #: src/views/config/ConfigList.vue:116
msgid "Create Folder" msgid "Create Folder"
msgstr "" msgstr ""
@ -449,9 +450,9 @@ msgid "Customize the name of local server to be displayed in the environment ind
msgstr "" msgstr ""
#: src/routes/index.ts:39 #: src/routes/index.ts:39
#: src/views/config/Config.vue:57
#: src/views/config/ConfigEditor.vue:118 #: src/views/config/ConfigEditor.vue:118
#: src/views/config/ConfigEditor.vue:79 #: src/views/config/ConfigEditor.vue:79
#: src/views/config/ConfigList.vue:57
msgid "Dashboard" msgid "Dashboard"
msgstr "" msgstr ""
@ -768,6 +769,10 @@ msgstr ""
msgid "Encrypt website with Let's Encrypt" msgid "Encrypt website with Let's Encrypt"
msgstr "" msgstr ""
#: src/views/config/ConfigList.vue:151
msgid "Enter"
msgstr ""
#: src/routes/index.ts:228 #: src/routes/index.ts:228
#: src/views/environment/Environment.vue:34 #: src/views/environment/Environment.vue:34
msgid "Environment" msgid "Environment"
@ -1129,9 +1134,9 @@ msgid "Make sure you have configured a reverse proxy for .well-known directory t
msgstr "" msgstr ""
#: src/routes/index.ts:102 #: src/routes/index.ts:102
#: src/views/config/Config.vue:62
#: src/views/config/ConfigEditor.vue:123 #: src/views/config/ConfigEditor.vue:123
#: src/views/config/ConfigEditor.vue:84 #: src/views/config/ConfigEditor.vue:84
#: src/views/config/ConfigList.vue:62
msgid "Manage Configs" msgid "Manage Configs"
msgstr "" msgstr ""
@ -1178,6 +1183,7 @@ msgstr ""
#: src/components/ChatGPT/ChatGPT.vue:248 #: src/components/ChatGPT/ChatGPT.vue:248
#: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
#: src/components/StdDesign/StdDataDisplay/StdTable.vue:532 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
#: src/views/config/ConfigList.vue:151
msgid "Modify" msgid "Modify"
msgstr "" msgstr ""
@ -1623,7 +1629,7 @@ msgid "Removed successfully"
msgstr "" msgstr ""
#: src/views/config/components/Rename.vue:52 #: src/views/config/components/Rename.vue:52
#: src/views/config/Config.vue:130 #: src/views/config/ConfigList.vue:159
#: src/views/domain/ngx_conf/NgxUpstream.vue:123 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
msgid "Rename" msgid "Rename"
msgstr "" msgstr ""

View file

@ -195,9 +195,9 @@ msgid "Auto-renewal enabled for %{name}"
msgstr "Автообновление включено для %{name}" msgstr "Автообновление включено для %{name}"
#: src/views/certificate/CertificateEditor.vue:247 #: src/views/certificate/CertificateEditor.vue:247
#: src/views/config/Config.vue:143 src/views/config/ConfigEditor.vue:196 #: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168 #: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
#: src/views/stream/StreamEdit.vue:245 #: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
msgid "Back" msgid "Back"
msgstr "Назад" msgstr "Назад"
@ -371,7 +371,7 @@ msgstr "Проверка конфигурации успешна"
msgid "Configuration Name" msgid "Configuration Name"
msgstr "Название конфигурации" msgstr "Название конфигурации"
#: src/views/config/Config.vue:91 #: src/views/config/ConfigList.vue:91
msgid "Configurations" msgid "Configurations"
msgstr "Конфигурации" msgstr "Конфигурации"
@ -422,12 +422,12 @@ msgstr "Создан в"
msgid "Create Another" msgid "Create Another"
msgstr "Создать еще" msgstr "Создать еще"
#: src/views/config/Config.vue:99 #: src/views/config/ConfigList.vue:109
#, fuzzy #, fuzzy
msgid "Create File" msgid "Create File"
msgstr "Создан в" msgstr "Создан в"
#: src/views/config/components/Mkdir.vue:50 src/views/config/Config.vue:100 #: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
#, fuzzy #, fuzzy
msgid "Create Folder" msgid "Create Folder"
msgstr "Создать еще" msgstr "Создать еще"
@ -476,8 +476,8 @@ msgid ""
"indicator." "indicator."
msgstr "" msgstr ""
#: src/routes/index.ts:39 src/views/config/Config.vue:57 #: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
#: src/views/config/ConfigEditor.vue:118 src/views/config/ConfigEditor.vue:79 #: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
msgid "Dashboard" msgid "Dashboard"
msgstr "Доска" msgstr "Доска"
@ -807,6 +807,10 @@ msgstr "Активировано успешно"
msgid "Encrypt website with Let's Encrypt" msgid "Encrypt website with Let's Encrypt"
msgstr "Использовать для сайта Let's Encrypt" msgstr "Использовать для сайта Let's Encrypt"
#: src/views/config/ConfigList.vue:151
msgid "Enter"
msgstr ""
#: src/routes/index.ts:228 src/views/environment/Environment.vue:34 #: src/routes/index.ts:228 src/views/environment/Environment.vue:34
msgid "Environment" msgid "Environment"
msgstr "Окружение" msgstr "Окружение"
@ -1199,8 +1203,8 @@ msgstr ""
"Убедитесь, что вы настроили обратный прокси-сервер для каталога .well-known " "Убедитесь, что вы настроили обратный прокси-сервер для каталога .well-known "
"на HTTPChallengePort перед получением сертификата»." "на HTTPChallengePort перед получением сертификата»."
#: src/routes/index.ts:102 src/views/config/Config.vue:62 #: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
#: src/views/config/ConfigEditor.vue:123 src/views/config/ConfigEditor.vue:84 #: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
msgid "Manage Configs" msgid "Manage Configs"
msgstr "Конфигурации" msgstr "Конфигурации"
@ -1247,6 +1251,7 @@ msgstr "Расширенный режим"
#: src/components/ChatGPT/ChatGPT.vue:248 #: src/components/ChatGPT/ChatGPT.vue:248
#: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
#: src/components/StdDesign/StdDataDisplay/StdTable.vue:532 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
#: src/views/config/ConfigList.vue:151
#, fuzzy #, fuzzy
msgid "Modify" msgid "Modify"
msgstr "Изменить" msgstr "Изменить"
@ -1716,7 +1721,8 @@ msgstr "Успешно сохранено"
msgid "Removed successfully" msgid "Removed successfully"
msgstr "Успешно сохранено" msgstr "Успешно сохранено"
#: src/views/config/components/Rename.vue:52 src/views/config/Config.vue:130 #: src/views/config/components/Rename.vue:52
#: src/views/config/ConfigList.vue:159
#: src/views/domain/ngx_conf/NgxUpstream.vue:123 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
#, fuzzy #, fuzzy
msgid "Rename" msgid "Rename"

View file

@ -195,9 +195,9 @@ msgid "Auto-renewal enabled for %{name}"
msgstr "Đã bật tự động gia hạn SSL cho %{name}" msgstr "Đã bật tự động gia hạn SSL cho %{name}"
#: src/views/certificate/CertificateEditor.vue:247 #: src/views/certificate/CertificateEditor.vue:247
#: src/views/config/Config.vue:143 src/views/config/ConfigEditor.vue:196 #: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168 #: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
#: src/views/stream/StreamEdit.vue:245 #: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
msgid "Back" msgid "Back"
msgstr "Quay lại" msgstr "Quay lại"
@ -371,7 +371,7 @@ msgstr "Tệp cấu hình được kiểm tra thành công"
msgid "Configuration Name" msgid "Configuration Name"
msgstr "Tên cấu hình" msgstr "Tên cấu hình"
#: src/views/config/Config.vue:91 #: src/views/config/ConfigList.vue:91
msgid "Configurations" msgid "Configurations"
msgstr "Cấu hình" msgstr "Cấu hình"
@ -422,12 +422,12 @@ msgstr "Ngày tạo"
msgid "Create Another" msgid "Create Another"
msgstr "Tạo thêm" msgstr "Tạo thêm"
#: src/views/config/Config.vue:99 #: src/views/config/ConfigList.vue:109
#, fuzzy #, fuzzy
msgid "Create File" msgid "Create File"
msgstr "Ngày tạo" msgstr "Ngày tạo"
#: src/views/config/components/Mkdir.vue:50 src/views/config/Config.vue:100 #: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
#, fuzzy #, fuzzy
msgid "Create Folder" msgid "Create Folder"
msgstr "Tạo thêm" msgstr "Tạo thêm"
@ -476,8 +476,8 @@ msgid ""
"indicator." "indicator."
msgstr "" msgstr ""
#: src/routes/index.ts:39 src/views/config/Config.vue:57 #: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
#: src/views/config/ConfigEditor.vue:118 src/views/config/ConfigEditor.vue:79 #: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
msgid "Dashboard" msgid "Dashboard"
msgstr "Bảng điều khiển" msgstr "Bảng điều khiển"
@ -808,6 +808,10 @@ msgstr "Đã bật"
msgid "Encrypt website with Let's Encrypt" msgid "Encrypt website with Let's Encrypt"
msgstr "Bảo mật trang web với Let's Encrypt" msgstr "Bảo mật trang web với Let's Encrypt"
#: src/views/config/ConfigList.vue:151
msgid "Enter"
msgstr ""
#: src/routes/index.ts:228 src/views/environment/Environment.vue:34 #: src/routes/index.ts:228 src/views/environment/Environment.vue:34
msgid "Environment" msgid "Environment"
msgstr "Environment" msgstr "Environment"
@ -1201,8 +1205,8 @@ msgstr ""
"Đảm bảo rằng bạn đã định cấu hình proxy ngược (reverse proxy) thư mục .well-" "Đả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." "known tới HTTPChallengePort (default: 9180) trước khi ký chứng chỉ SSL."
#: src/routes/index.ts:102 src/views/config/Config.vue:62 #: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
#: src/views/config/ConfigEditor.vue:123 src/views/config/ConfigEditor.vue:84 #: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
msgid "Manage Configs" msgid "Manage Configs"
msgstr "Quản lý cấu hình" msgstr "Quản lý cấu hình"
@ -1248,6 +1252,7 @@ msgstr "Run Mode"
#: src/components/ChatGPT/ChatGPT.vue:248 #: src/components/ChatGPT/ChatGPT.vue:248
#: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
#: src/components/StdDesign/StdDataDisplay/StdTable.vue:532 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
#: src/views/config/ConfigList.vue:151
#, fuzzy #, fuzzy
msgid "Modify" msgid "Modify"
msgstr "Sửa" msgstr "Sửa"
@ -1718,7 +1723,8 @@ msgstr "Xoá thành công"
msgid "Removed successfully" msgid "Removed successfully"
msgstr "Xoá thành công" msgstr "Xoá thành công"
#: src/views/config/components/Rename.vue:52 src/views/config/Config.vue:130 #: src/views/config/components/Rename.vue:52
#: src/views/config/ConfigList.vue:159
#: src/views/domain/ngx_conf/NgxUpstream.vue:123 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
#, fuzzy #, fuzzy
msgid "Rename" msgid "Rename"

Binary file not shown.

View file

@ -184,9 +184,9 @@ msgid "Auto-renewal enabled for %{name}"
msgstr "成功启用 %{name} 自动续签" msgstr "成功启用 %{name} 自动续签"
#: src/views/certificate/CertificateEditor.vue:247 #: src/views/certificate/CertificateEditor.vue:247
#: src/views/config/Config.vue:143 src/views/config/ConfigEditor.vue:196 #: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168 #: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
#: src/views/stream/StreamEdit.vue:245 #: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
msgid "Back" msgid "Back"
msgstr "返回" msgstr "返回"
@ -344,7 +344,7 @@ msgstr "配置文件测试成功"
msgid "Configuration Name" msgid "Configuration Name"
msgstr "配置名称" msgstr "配置名称"
#: src/views/config/Config.vue:91 #: src/views/config/ConfigList.vue:91
msgid "Configurations" msgid "Configurations"
msgstr "配置" msgstr "配置"
@ -394,11 +394,11 @@ msgstr "创建"
msgid "Create Another" msgid "Create Another"
msgstr "再创建一个" msgstr "再创建一个"
#: src/views/config/Config.vue:99 #: src/views/config/ConfigList.vue:109
msgid "Create File" msgid "Create File"
msgstr "创建文件" msgstr "创建文件"
#: src/views/config/components/Mkdir.vue:50 src/views/config/Config.vue:100 #: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
msgid "Create Folder" msgid "Create Folder"
msgstr "创建文件夹" msgstr "创建文件夹"
@ -445,8 +445,8 @@ msgid ""
"indicator." "indicator."
msgstr "自定义显示在环境指示器中的本地服务器名称。" msgstr "自定义显示在环境指示器中的本地服务器名称。"
#: src/routes/index.ts:39 src/views/config/Config.vue:57 #: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
#: src/views/config/ConfigEditor.vue:118 src/views/config/ConfigEditor.vue:79 #: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
msgid "Dashboard" msgid "Dashboard"
msgstr "仪表盘" msgstr "仪表盘"
@ -751,6 +751,10 @@ msgstr "启用成功"
msgid "Encrypt website with Let's Encrypt" msgid "Encrypt website with Let's Encrypt"
msgstr "用 Let's Encrypt 对网站进行加密" msgstr "用 Let's Encrypt 对网站进行加密"
#: src/views/config/ConfigList.vue:151
msgid "Enter"
msgstr "进入"
#: src/routes/index.ts:228 src/views/environment/Environment.vue:34 #: src/routes/index.ts:228 src/views/environment/Environment.vue:34
msgid "Environment" msgid "Environment"
msgstr "环境" msgstr "环境"
@ -1123,8 +1127,8 @@ msgstr ""
"在获取签发证书前,请确保配置文件中已将 .well-known 目录反向代理到 " "在获取签发证书前,请确保配置文件中已将 .well-known 目录反向代理到 "
"HTTPChallengePort。" "HTTPChallengePort。"
#: src/routes/index.ts:102 src/views/config/Config.vue:62 #: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
#: src/views/config/ConfigEditor.vue:123 src/views/config/ConfigEditor.vue:84 #: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
msgid "Manage Configs" msgid "Manage Configs"
msgstr "配置管理" msgstr "配置管理"
@ -1168,6 +1172,7 @@ msgstr "模型"
#: src/components/ChatGPT/ChatGPT.vue:248 #: src/components/ChatGPT/ChatGPT.vue:248
#: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
#: src/components/StdDesign/StdDataDisplay/StdTable.vue:532 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
#: src/views/config/ConfigList.vue:151
msgid "Modify" msgid "Modify"
msgstr "修改" msgstr "修改"
@ -1612,7 +1617,8 @@ msgstr "移除成功"
msgid "Removed successfully" msgid "Removed successfully"
msgstr "删除成功" msgstr "删除成功"
#: src/views/config/components/Rename.vue:52 src/views/config/Config.vue:130 #: src/views/config/components/Rename.vue:52
#: src/views/config/ConfigList.vue:159
#: src/views/domain/ngx_conf/NgxUpstream.vue:123 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
msgid "Rename" msgid "Rename"
msgstr "重命名" msgstr "重命名"

View file

@ -197,9 +197,9 @@ msgid "Auto-renewal enabled for %{name}"
msgstr "已啟用 %{name} 的自動續簽" msgstr "已啟用 %{name} 的自動續簽"
#: src/views/certificate/CertificateEditor.vue:247 #: src/views/certificate/CertificateEditor.vue:247
#: src/views/config/Config.vue:143 src/views/config/ConfigEditor.vue:196 #: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168 #: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
#: src/views/stream/StreamEdit.vue:245 #: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
msgid "Back" msgid "Back"
msgstr "返回" msgstr "返回"
@ -366,7 +366,7 @@ msgstr "設定檔案測試成功"
msgid "Configuration Name" msgid "Configuration Name"
msgstr "設定名稱" msgstr "設定名稱"
#: src/views/config/Config.vue:91 #: src/views/config/ConfigList.vue:91
msgid "Configurations" msgid "Configurations"
msgstr "設定" msgstr "設定"
@ -417,12 +417,12 @@ msgstr "建立時間"
msgid "Create Another" msgid "Create Another"
msgstr "再建立一個" msgstr "再建立一個"
#: src/views/config/Config.vue:99 #: src/views/config/ConfigList.vue:109
#, fuzzy #, fuzzy
msgid "Create File" msgid "Create File"
msgstr "建立時間" msgstr "建立時間"
#: src/views/config/components/Mkdir.vue:50 src/views/config/Config.vue:100 #: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
#, fuzzy #, fuzzy
msgid "Create Folder" msgid "Create Folder"
msgstr "再建立一個" msgstr "再建立一個"
@ -471,8 +471,8 @@ msgid ""
"indicator." "indicator."
msgstr "" msgstr ""
#: src/routes/index.ts:39 src/views/config/Config.vue:57 #: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
#: src/views/config/ConfigEditor.vue:118 src/views/config/ConfigEditor.vue:79 #: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
msgid "Dashboard" msgid "Dashboard"
msgstr "儀表板" msgstr "儀表板"
@ -788,6 +788,10 @@ msgstr "成功啟用"
msgid "Encrypt website with Let's Encrypt" msgid "Encrypt website with Let's Encrypt"
msgstr "用 Let's Encrypt 對網站進行加密" msgstr "用 Let's Encrypt 對網站進行加密"
#: src/views/config/ConfigList.vue:151
msgid "Enter"
msgstr ""
#: src/routes/index.ts:228 src/views/environment/Environment.vue:34 #: src/routes/index.ts:228 src/views/environment/Environment.vue:34
msgid "Environment" msgid "Environment"
msgstr "環境" msgstr "環境"
@ -1172,8 +1176,8 @@ msgid ""
msgstr "" msgstr ""
"在取得憑證前,請確保您已將 .well-known 目錄反向代理到 HTTPChallengePort。" "在取得憑證前,請確保您已將 .well-known 目錄反向代理到 HTTPChallengePort。"
#: src/routes/index.ts:102 src/views/config/Config.vue:62 #: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
#: src/views/config/ConfigEditor.vue:123 src/views/config/ConfigEditor.vue:84 #: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
msgid "Manage Configs" msgid "Manage Configs"
msgstr "管理設定" msgstr "管理設定"
@ -1220,6 +1224,7 @@ msgstr "執行模式"
#: src/components/ChatGPT/ChatGPT.vue:248 #: src/components/ChatGPT/ChatGPT.vue:248
#: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181 #: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
#: src/components/StdDesign/StdDataDisplay/StdTable.vue:532 #: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
#: src/views/config/ConfigList.vue:151
msgid "Modify" msgid "Modify"
msgstr "修改" msgstr "修改"
@ -1681,7 +1686,8 @@ msgstr "儲存成功"
msgid "Removed successfully" msgid "Removed successfully"
msgstr "儲存成功" msgstr "儲存成功"
#: src/views/config/components/Rename.vue:52 src/views/config/Config.vue:130 #: src/views/config/components/Rename.vue:52
#: src/views/config/ConfigList.vue:159
#: src/views/domain/ngx_conf/NgxUpstream.vue:123 #: src/views/domain/ngx_conf/NgxUpstream.vue:123
#, fuzzy #, fuzzy
msgid "Rename" msgid "Rename"

View file

@ -1,11 +1,13 @@
import type { AxiosRequestConfig } from 'axios' import type { AxiosRequestConfig } from 'axios'
import axios from 'axios' import axios from 'axios'
import { useCookies } from '@vueuse/integrations/useCookies'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import NProgress from 'nprogress' import NProgress from 'nprogress'
import { useSettingsStore, useUserStore } from '@/pinia' import { useSettingsStore, useUserStore } from '@/pinia'
import 'nprogress/nprogress.css' import 'nprogress/nprogress.css'
import router from '@/routes' import router from '@/routes'
import useOTPModal from '@/components/OTP/useOTPModal'
const user = useUserStore() const user = useUserStore()
const settings = useSettingsStore() const settings = useSettingsStore()
@ -58,8 +60,14 @@ instance.interceptors.response.use(
}, },
async error => { async error => {
NProgress.done() NProgress.done()
const otpModal = useOTPModal()
const cookies = useCookies(['nginx-ui-2fa'])
switch (error.response.status) { switch (error.response.status) {
case 401: case 401:
cookies.remove('secure_session_id')
await otpModal.open()
break
case 403: case 403:
user.logout() user.logout()
await router.push('/login') await router.push('/login')

View file

@ -97,7 +97,7 @@ export const routes: RouteRecordRaw[] = [
{ {
path: 'config', path: 'config',
name: 'Manage Configs', name: 'Manage Configs',
component: () => import('@/views/config/Config.vue'), component: () => import('@/views/config/ConfigList.vue'),
meta: { meta: {
name: () => $gettext('Manage Configs'), name: () => $gettext('Manage Configs'),
icon: FileOutlined, icon: FileOutlined,

View file

@ -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}

View file

@ -1,8 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import { InfoCircleOutlined } from '@ant-design/icons-vue'
import { formatDateTime } from '@/lib/helper' import { formatDateTime } from '@/lib/helper'
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue' import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
import type { Config } from '@/api/config'
import config from '@/api/config' import config from '@/api/config'
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue' import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
import ngx from '@/api/ngx' import ngx from '@/api/ngx'
@ -10,7 +12,10 @@ import InspectConfig from '@/views/config/InspectConfig.vue'
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue' import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
import type { ChatComplicationMessage } from '@/api/openai' import type { ChatComplicationMessage } from '@/api/openai'
import { useBreadcrumbs } from '@/composables/useBreadcrumbs' import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
import { useSettingsStore } from '@/pinia'
const settings = useSettingsStore()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const refForm = ref() const refForm = ref()
@ -32,10 +37,12 @@ const data = ref({
name: '', name: '',
content: '', content: '',
filepath: '', filepath: '',
}) sync_node_ids: [] as number[],
sync_overwrite: false,
} as Config)
const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]> const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]>
const activeKey = ref(['basic', 'chatgpt']) const activeKey = ref(['basic', 'deploy', 'chatgpt'])
const modifiedAt = ref('') const modifiedAt = ref('')
const nginxConfigBase = ref('') const nginxConfigBase = ref('')
@ -145,6 +152,8 @@ function save() {
filepath: data.value.filepath, filepath: data.value.filepath,
new_filepath: newPath.value, new_filepath: newPath.value,
content: data.value.content, content: data.value.content,
sync_node_ids: data.value.sync_node_ids,
sync_overwrite: data.value.sync_overwrite,
}).then(r => { }).then(r => {
data.value.content = r.content data.value.content = r.content
message.success($gettext('Saved successfully')) message.success($gettext('Saved successfully'))
@ -261,6 +270,29 @@ function goBack() {
</AFormItem> </AFormItem>
</AForm> </AForm>
</ACollapsePanel> </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 <ACollapsePanel
key="chatgpt" key="chatgpt"
header="ChatGPT" header="ChatGPT"
@ -295,4 +327,19 @@ function goBack() {
:deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) { :deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
padding: 0 0 10px 0; 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> </style>

View file

@ -90,14 +90,31 @@ const refRename = ref()
<template> <template>
<ACard :title="$gettext('Configurations')"> <ACard :title="$gettext('Configurations')">
<template #extra> <template #extra>
<a <AButton
class="mr-4" v-if="basePath"
type="link"
size="small"
@click="goBack"
>
{{ $gettext('Back') }}
</AButton>
<AButton
type="link"
size="small"
@click="router.push({ @click="router.push({
path: '/config/add', path: '/config/add',
query: { basePath: basePath || undefined }, query: { basePath: basePath || undefined },
})" })"
>{{ $gettext('Create File') }}</a> >
<a @click="() => refMkdir.open(basePath)">{{ $gettext('Create Folder') }}</a> {{ $gettext('Create File') }}
</AButton>
<AButton
type="link"
size="small"
@click="() => refMkdir.open(basePath)"
>
{{ $gettext('Create Folder') }}
</AButton>
</template> </template>
<InspectConfig ref="refInspectConfig" /> <InspectConfig ref="refInspectConfig" />
<StdTable <StdTable
@ -110,24 +127,37 @@ const refRename = ref()
row-key="name" row-key="name"
:get-params="getParams" :get-params="getParams"
disable-query-params disable-query-params
@click-edit="(r, row) => { disable-modify
if (!row.is_dir) { >
<template #actions="{ record }">
<AButton
type="link"
size="small"
@click="() => {
if (!record.is_dir) {
$router.push({ $router.push({
path: `/config/${basePath}${r}/edit`, path: `/config/${basePath}${record.name}/edit`,
}) })
} }
else { else {
$router.push({ $router.push({
query: { query: {
dir: basePath + r, dir: basePath + record.name,
}, },
}) })
} }
}" }"
> >
<template #actions="{ record }"> {{ $gettext('Modify') }}
</AButton>
<ADivider type="vertical" /> <ADivider type="vertical" />
<a @click="() => refRename.open(basePath, record.name)">{{ $gettext('Rename') }}</a> <AButton
type="link"
size="small"
@click="() => refRename.open(basePath, record.name, record.is_dir)"
>
{{ $gettext('Rename') }}
</AButton>
</template> </template>
</StdTable> </StdTable>
<Mkdir <Mkdir

View file

@ -27,8 +27,7 @@ function ok() {
refForm.value.validate().then(() => { refForm.value.validate().then(() => {
const otpModal = useOTPModal() const otpModal = useOTPModal()
otpModal.open({ otpModal.open().then(() => {
onOk() {
config.mkdir(data.value.basePath, data.value.name).then(() => { config.mkdir(data.value.basePath, data.value.name).then(() => {
visible.value = false visible.value = false
@ -37,7 +36,6 @@ function ok() {
}).catch(e => { }).catch(e => {
message.error(`${$gettext('Server error')} ${e?.message}`) message.error(`${$gettext('Server error')} ${e?.message}`)
}) })
},
}) })
}) })
} }

View file

@ -2,22 +2,27 @@
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import config from '@/api/config' import config from '@/api/config'
import useOTPModal from '@/components/OTP/useOTPModal' import useOTPModal from '@/components/OTP/useOTPModal'
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
const emit = defineEmits(['renamed']) const emit = defineEmits(['renamed'])
const visible = ref(false) const visible = ref(false)
const isDirFlag = ref(false)
const data = ref({ const data = ref({
basePath: '', basePath: '',
orig_name: '', orig_name: '',
new_name: '', new_name: '',
sync_node_ids: [] as number[],
}) })
const refForm = ref() const refForm = ref()
function open(basePath: string, origName: string) {
function open(basePath: string, origName: string, isDir: boolean) {
visible.value = true visible.value = true
data.value.orig_name = origName data.value.orig_name = origName
data.value.new_name = origName data.value.new_name = origName
data.value.basePath = basePath data.value.basePath = basePath
isDirFlag.value = isDir
} }
defineExpose({ defineExpose({
@ -26,20 +31,18 @@ defineExpose({
function ok() { function ok() {
refForm.value.validate().then(() => { refForm.value.validate().then(() => {
const { basePath, orig_name, new_name } = data.value const { basePath, orig_name, new_name, sync_node_ids } = data.value
const otpModal = useOTPModal() const otpModal = useOTPModal()
otpModal.open({ otpModal.open().then(() => {
onOk() { config.rename(basePath, orig_name, new_name, sync_node_ids).then(() => {
config.rename(basePath, orig_name, new_name).then(() => {
visible.value = false visible.value = false
message.success($gettext('Rename successfully')) message.success($gettext('Rename successfully'))
emit('renamed') emit('renamed')
}).catch(e => { }).catch(e => {
message.error(`${$gettext('Server error')} ${e?.message}`) message.error(`${$gettext('Server error')} ${e?.message}`)
}) })
},
}) })
}) })
} }
@ -72,6 +75,15 @@ function ok() {
> >
<AInput v-model:value="data.new_name" /> <AInput v-model:value="data.new_name" />
</AFormItem> </AFormItem>
<AFormItem
v-if="isDirFlag"
:label="$gettext('Sync')"
>
<NodeSelector
v-model:target="data.sync_node_ids"
hidden-local
/>
</AFormItem>
</AForm> </AForm>
</AModal> </AModal>
</template> </template>

View file

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

View file

@ -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}

292
internal/config/sync.go Normal file
View file

@ -0,0 +1,292 @@
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 := &SyncConfigPayload{
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 Configuration Error", string(notificationPayloadBytes))
return
}
notification.Success("Sync Configuration Success", string(notificationPayloadBytes))
// handle rename
if p.NewFilepath == "" || p.Filepath == p.NewFilepath {
return
}
payloadBytes, err = json.Marshal(gin.H{
"base_path": filepath.Dir(p.Filepath),
"old_filepath": filepath.Base(p.Filepath),
"new_filepath": 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 = &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 Rename Configuration Error", string(notificationPayloadBytes))
return
}
notification.Success("Sync Rename Configuration Success", string(notificationPayloadBytes))
return
}
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 *SyncConfigPayload) 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("Sync Rename Configuration Error", string(notificationPayloadBytes))
return
}
notification.Success("Sync Rename Configuration Success", string(notificationPayloadBytes))
return
}

View file

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

View file

@ -1,11 +1,10 @@
package router package middleware
import ( import (
"encoding/base64" "encoding/base64"
"github.com/0xJacky/Nginx-UI/app" "github.com/0xJacky/Nginx-UI/app"
"github.com/0xJacky/Nginx-UI/internal/logger" "github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/internal/user" "github.com/0xJacky/Nginx-UI/internal/user"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/settings" "github.com/0xJacky/Nginx-UI/settings"
"github.com/gin-contrib/static" "github.com/gin-contrib/static"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -16,7 +15,7 @@ import (
"strings" "strings"
) )
func recovery() gin.HandlerFunc { func Recovery() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
defer func() { defer func() {
if err := recover(); err != nil { 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) { return func(c *gin.Context) {
abortWithAuthFailure := func() { abortWithAuthFailure := func() {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{ c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
@ -75,46 +74,11 @@ func authRequired() gin.HandlerFunc {
} }
} }
func required2FA() gin.HandlerFunc { type ServerFileSystemType struct {
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 {
http.FileSystem 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)) file, err := f.Open(path.Join(prefix, _path))
if file != nil { if file != nil {
defer func(file http.File) { defer func(file http.File) {
@ -127,7 +91,7 @@ func (f serverFileSystemType) Exists(prefix string, _path string) bool {
return err == nil 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)) sub, err := fs.Sub(app.DistFS, path.Join("dist", dir))
@ -136,14 +100,14 @@ func mustFS(dir string) (serverFileSystem static.ServeFileSystem) {
return return
} }
serverFileSystem = serverFileSystemType{ serverFileSystem = ServerFileSystemType{
http.FS(sub), http.FS(sub),
} }
return return
} }
func cacheJs() gin.HandlerFunc { func CacheJs() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if strings.Contains(c.Request.URL.String(), "js") { if strings.Contains(c.Request.URL.String(), "js") {
c.Header("Cache-Control", "max-age: 1296000") c.Header("Cache-Control", "max-age: 1296000")

View file

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

View file

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

View file

@ -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
model/config.go Normal file
View file

@ -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"`
}

View file

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

View file

@ -45,6 +45,8 @@ func newCert(db *gorm.DB, opts ...gen.DOOption) cert {
_cert.Log = field.NewString(tableName, "log") _cert.Log = field.NewString(tableName, "log")
_cert.Resource = field.NewField(tableName, "resource") _cert.Resource = field.NewField(tableName, "resource")
_cert.SyncNodeIds = field.NewField(tableName, "sync_node_ids") _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{ _cert.DnsCredential = certBelongsToDnsCredential{
db: db.Session(&gorm.Session{}), db: db.Session(&gorm.Session{}),
@ -83,6 +85,8 @@ type cert struct {
Log field.String Log field.String
Resource field.Field Resource field.Field
SyncNodeIds field.Field SyncNodeIds field.Field
MustStaple field.Bool
LegoDisableCNAMESupport field.Bool
DnsCredential certBelongsToDnsCredential DnsCredential certBelongsToDnsCredential
ACMEUser certBelongsToACMEUser ACMEUser certBelongsToACMEUser
@ -119,6 +123,8 @@ func (c *cert) updateTableName(table string) *cert {
c.Log = field.NewString(table, "log") c.Log = field.NewString(table, "log")
c.Resource = field.NewField(table, "resource") c.Resource = field.NewField(table, "resource")
c.SyncNodeIds = field.NewField(table, "sync_node_ids") 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() c.fillFieldMap()
@ -135,7 +141,7 @@ func (c *cert) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
} }
func (c *cert) fillFieldMap() { 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["id"] = c.ID
c.fieldMap["created_at"] = c.CreatedAt c.fieldMap["created_at"] = c.CreatedAt
c.fieldMap["updated_at"] = c.UpdatedAt c.fieldMap["updated_at"] = c.UpdatedAt
@ -153,6 +159,8 @@ func (c *cert) fillFieldMap() {
c.fieldMap["log"] = c.Log c.fieldMap["log"] = c.Log
c.fieldMap["resource"] = c.Resource c.fieldMap["resource"] = c.Resource
c.fieldMap["sync_node_ids"] = c.SyncNodeIds c.fieldMap["sync_node_ids"] = c.SyncNodeIds
c.fieldMap["must_staple"] = c.MustStaple
c.fieldMap["lego_disable_cname_support"] = c.LegoDisableCNAMESupport
} }

370
query/configs.gen.go Normal file
View file

@ -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
}

View file

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

View file

@ -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
}

View file

@ -16,6 +16,7 @@ import (
"github.com/0xJacky/Nginx-UI/api/terminal" "github.com/0xJacky/Nginx-UI/api/terminal"
"github.com/0xJacky/Nginx-UI/api/upstream" "github.com/0xJacky/Nginx-UI/api/upstream"
"github.com/0xJacky/Nginx-UI/api/user" "github.com/0xJacky/Nginx-UI/api/user"
"github.com/0xJacky/Nginx-UI/internal/middleware"
"github.com/gin-contrib/static" "github.com/gin-contrib/static"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"net/http" "net/http"
@ -23,14 +24,13 @@ import (
func InitRouter() *gin.Engine { func InitRouter() *gin.Engine {
r := gin.New() r := gin.New()
r.Use(gin.Logger()) r.Use(
r.Use(recovery()) gin.Logger(),
r.Use(cacheJs()) middleware.Recovery(),
r.Use(ipWhiteList()) middleware.CacheJs(),
middleware.IPWhiteList(),
//r.Use(OperationSync()) static.Serve("/", middleware.MustFs("")),
)
r.Use(static.Serve("/", mustFS("")))
r.NoRoute(func(c *gin.Context) { r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{ c.JSON(http.StatusNotFound, gin.H{
@ -44,7 +44,7 @@ func InitRouter() *gin.Engine {
user.InitAuthRouter(root) user.InitAuthRouter(root)
// Authorization required not websocket request // Authorization required not websocket request
g := root.Group("/", authRequired(), proxy()) g := root.Group("/", middleware.AuthRequired(), middleware.Proxy())
{ {
user.InitUserRouter(g) user.InitUserRouter(g)
analytic.InitRouter(g) analytic.InitRouter(g)
@ -65,11 +65,11 @@ func InitRouter() *gin.Engine {
} }
// Authorization required and websocket request // Authorization required and websocket request
w := root.Group("/", authRequired(), proxyWs()) w := root.Group("/", middleware.AuthRequired(), middleware.ProxyWs())
{ {
analytic.InitWebSocketRouter(w) analytic.InitWebSocketRouter(w)
certificate.InitCertificateWebSocketRouter(w) certificate.InitCertificateWebSocketRouter(w)
o := w.Group("", required2FA()) o := w.Group("", middleware.RequireSecureSession())
{ {
terminal.InitRouter(o) terminal.InitRouter(o)
} }