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/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai"
"net/http"
"os"
"path/filepath"
"time"
)
@ -18,6 +21,7 @@ func AddConfig(c *gin.Context) {
NewFilepath string `json:"new_filepath" binding:"required"`
Content string `json:"content"`
Overwrite bool `json:"overwrite"`
SyncNodeIds []int `json:"sync_node_ids"`
}
if !api.BindAndValid(c, &json) {
@ -41,6 +45,16 @@ func AddConfig(c *gin.Context) {
return
}
// check if the dir exists, if not, use mkdirAll to create the dir
dir := filepath.Dir(path)
if !helper.FileExists(dir) {
err := os.MkdirAll(dir, 0755)
if err != nil {
api.ErrHandler(c, err)
return
}
}
err := os.WriteFile(path, []byte(content), 0644)
if err != nil {
api.ErrHandler(c, err)
@ -55,6 +69,24 @@ func AddConfig(c *gin.Context) {
return
}
q := query.Config
_, err = q.Where(q.Filepath.Eq(path)).Delete()
if err != nil {
api.ErrHandler(c, err)
return
}
err = q.Create(&model.Config{
Name: name,
Filepath: path,
SyncNodeIds: json.SyncNodeIds,
SyncOverwrite: json.Overwrite,
})
if err != nil {
api.ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, config.Config{
Name: name,
Content: content,

View file

@ -12,6 +12,12 @@ import (
"os"
)
type APIConfigResp struct {
config.Config
SyncNodeIds []int `json:"sync_node_ids" gorm:"serializer:json"`
SyncOverwrite bool `json:"sync_overwrite"`
}
func GetConfig(c *gin.Context) {
name := c.Param("name")
@ -34,7 +40,7 @@ func GetConfig(c *gin.Context) {
api.ErrHandler(c, err)
return
}
q := query.Config
g := query.ChatGPTLog
chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
if err != nil {
@ -46,11 +52,21 @@ func GetConfig(c *gin.Context) {
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(),
Content: string(content),
ChatGPTMessages: chatgpt.Content,
FilePath: path,
ModifiedAt: stat.ModTime(),
},
SyncNodeIds: cfg.SyncNodeIds,
SyncOverwrite: cfg.SyncOverwrite,
})
}

View file

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

View file

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

View file

@ -2,7 +2,9 @@ package config
import (
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/logger"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
@ -15,11 +17,13 @@ func Rename(c *gin.Context) {
BasePath string `json:"base_path"`
OrigName string `json:"orig_name"`
NewName string `json:"new_name"`
SyncNodeIds []int `json:"sync_node_ids" gorm:"serializer:json"`
}
if !api.BindAndValid(c, &json) {
return
}
if json.OrigName == json.OrigName {
logger.Debug(json)
if json.OrigName == json.NewName {
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
@ -55,11 +59,36 @@ func Rename(c *gin.Context) {
return
}
if !stat.IsDir() {
// update ChatGPT records
g := query.ChatGPTLog
q := query.Config
cfg, err := q.Where(q.Filepath.Eq(origFullPath)).FirstOrInit()
if err != nil {
api.ErrHandler(c, err)
return
}
if !stat.IsDir() {
_, _ = g.Where(g.Name.Eq(newFullPath)).Delete()
_, _ = g.Where(g.Name.Eq(origFullPath)).Update(g.Name, newFullPath)
// for file, the sync policy for this file is used
json.SyncNodeIds = cfg.SyncNodeIds
} else {
// is directory, update all records under the directory
_, _ = g.Where(g.Name.Like(origFullPath+"%")).Update(g.Name, g.Name.Replace(origFullPath, newFullPath))
}
_, err = q.Where(q.Filepath.Eq(origFullPath)).Update(q.Filepath, newFullPath)
if err != nil {
api.ErrHandler(c, err)
return
}
if len(json.SyncNodeIds) > 0 {
err = config.SyncRenameOnRemoteServer(origFullPath, newFullPath, json.SyncNodeIds)
if err != nil {
api.ErrHandler(c, err)
return
}
}
c.JSON(http.StatusOK, gin.H{

View file

@ -1,6 +1,9 @@
package config
import "github.com/gin-gonic/gin"
import (
"github.com/0xJacky/Nginx-UI/internal/middleware"
"github.com/gin-gonic/gin"
)
func InitRouter(r *gin.RouterGroup) {
r.GET("config_base_path", GetBasePath)
@ -9,6 +12,10 @@ func InitRouter(r *gin.RouterGroup) {
r.GET("config/*name", GetConfig)
r.POST("config", AddConfig)
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) {
var json struct {
OTP string `json:"otp"`

View file

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

View file

@ -8,6 +8,8 @@ export interface Config {
chatgpt_messages: ChatComplicationMessage[]
filepath: string
modified_at: string
sync_node_ids?: number[]
sync_overwrite?: false
}
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 })
}
rename(basePath: string, origName: string, newName: string) {
return http.post('/config_rename', { base_path: basePath, orig_name: origName, new_name: newName })
rename(basePath: string, origName: string, newName: string, syncNodeIds: number[]) {
return http.post('/config_rename', {
base_path: basePath,
orig_name: origName,
new_name: newName,
sync_node_ids: syncNodeIds,
})
}
}

View file

@ -24,6 +24,9 @@ const otp = {
recovery_code,
})
},
secure_session_status() {
return http.get('/otp_secure_session_status')
},
}
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 { syncCertificateError, syncCertificateSuccess } from '@/components/Notification/cert'
import {
syncConfigError,
syncConfigSuccess,
syncRenameConfigError,
syncRenameConfigSuccess,
} from '@/components/Notification/config'
export const detailRender = (args: customRender) => {
switch (args.record.title) {
@ -6,26 +13,15 @@ export const detailRender = (args: customRender) => {
return syncCertificateSuccess(args.text)
case 'Sync Certificate Error':
return syncCertificateError(args.text)
case '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:
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 { useUserStore } from '@/pinia'
export interface OTPModalProps {
onOk?: (secureSessionId: string) => void
onCancel?: () => void
}
const useOTPModal = () => {
const refOTPAuthorization = ref<typeof OTPAuthorization>()
const randomId = Math.random().toString(36).substring(2, 8)
@ -26,10 +21,12 @@ const useOTPModal = () => {
document.head.appendChild(style)
}
const open = async ({ onOk, onCancel }: OTPModalProps) => {
const open = async (): Promise<string> => {
const { status } = await otp.status()
return new Promise((resolve, reject) => {
if (!status) {
onOk?.('')
resolve('')
return
}
@ -37,7 +34,7 @@ const useOTPModal = () => {
const cookies = useCookies(['nginx-ui-2fa'])
const ssid = cookies.get('secure_session_id')
if (ssid) {
onOk?.(ssid)
resolve(ssid)
secureSessionId.value = ssid
return
@ -56,7 +53,7 @@ const useOTPModal = () => {
const verify = (passcode: string, recovery: string) => {
otp.start_secure_session(passcode, recovery).then(r => {
cookies.set('secure_session_id', r.session_id, { maxAge: 60 * 3 })
onOk?.(r.session_id)
resolve(r.session_id)
close()
secureSessionId.value = r.session_id
}).catch(async () => {
@ -74,7 +71,8 @@ const useOTPModal = () => {
footer: false,
onCancel: () => {
close()
onCancel?.()
// eslint-disable-next-line prefer-promise-reject-errors
reject()
},
}, {
default: () => h(
@ -87,7 +85,8 @@ const useOTPModal = () => {
),
})
render(vnode, container)
render(vnode, container!)
})
}
return { open }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -195,9 +195,9 @@ msgid "Auto-renewal enabled for %{name}"
msgstr "Автообновление включено для %{name}"
#: src/views/certificate/CertificateEditor.vue:247
#: src/views/config/Config.vue:143 src/views/config/ConfigEditor.vue:196
#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168
#: src/views/stream/StreamEdit.vue:245
#: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
#: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
#: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
msgid "Back"
msgstr "Назад"
@ -371,7 +371,7 @@ msgstr "Проверка конфигурации успешна"
msgid "Configuration Name"
msgstr "Название конфигурации"
#: src/views/config/Config.vue:91
#: src/views/config/ConfigList.vue:91
msgid "Configurations"
msgstr "Конфигурации"
@ -422,12 +422,12 @@ msgstr "Создан в"
msgid "Create Another"
msgstr "Создать еще"
#: src/views/config/Config.vue:99
#: src/views/config/ConfigList.vue:109
#, fuzzy
msgid "Create File"
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
msgid "Create Folder"
msgstr "Создать еще"
@ -476,8 +476,8 @@ msgid ""
"indicator."
msgstr ""
#: src/routes/index.ts:39 src/views/config/Config.vue:57
#: src/views/config/ConfigEditor.vue:118 src/views/config/ConfigEditor.vue:79
#: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
#: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
msgid "Dashboard"
msgstr "Доска"
@ -807,6 +807,10 @@ msgstr "Активировано успешно"
msgid "Encrypt website with 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
msgid "Environment"
msgstr "Окружение"
@ -1199,8 +1203,8 @@ msgstr ""
"Убедитесь, что вы настроили обратный прокси-сервер для каталога .well-known "
"на HTTPChallengePort перед получением сертификата»."
#: src/routes/index.ts:102 src/views/config/Config.vue:62
#: src/views/config/ConfigEditor.vue:123 src/views/config/ConfigEditor.vue:84
#: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
#: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
msgid "Manage Configs"
msgstr "Конфигурации"
@ -1247,6 +1251,7 @@ msgstr "Расширенный режим"
#: src/components/ChatGPT/ChatGPT.vue:248
#: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
#: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
#: src/views/config/ConfigList.vue:151
#, fuzzy
msgid "Modify"
msgstr "Изменить"
@ -1716,7 +1721,8 @@ msgstr "Успешно сохранено"
msgid "Removed successfully"
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
#, fuzzy
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}"
#: src/views/certificate/CertificateEditor.vue:247
#: src/views/config/Config.vue:143 src/views/config/ConfigEditor.vue:196
#: src/views/domain/DomainEdit.vue:253 src/views/nginx_log/NginxLog.vue:168
#: src/views/stream/StreamEdit.vue:245
#: src/views/config/ConfigEditor.vue:196 src/views/config/ConfigList.vue:173
#: src/views/config/ConfigList.vue:99 src/views/domain/DomainEdit.vue:253
#: src/views/nginx_log/NginxLog.vue:168 src/views/stream/StreamEdit.vue:245
msgid "Back"
msgstr "Quay lại"
@ -371,7 +371,7 @@ msgstr "Tệp cấu hình được kiểm tra thành công"
msgid "Configuration Name"
msgstr "Tên cấu hình"
#: src/views/config/Config.vue:91
#: src/views/config/ConfigList.vue:91
msgid "Configurations"
msgstr "Cấu hình"
@ -422,12 +422,12 @@ msgstr "Ngày tạo"
msgid "Create Another"
msgstr "Tạo thêm"
#: src/views/config/Config.vue:99
#: src/views/config/ConfigList.vue:109
#, fuzzy
msgid "Create File"
msgstr "Ngày tạo"
#: src/views/config/components/Mkdir.vue:50 src/views/config/Config.vue:100
#: src/views/config/components/Mkdir.vue:50 src/views/config/ConfigList.vue:116
#, fuzzy
msgid "Create Folder"
msgstr "Tạo thêm"
@ -476,8 +476,8 @@ msgid ""
"indicator."
msgstr ""
#: src/routes/index.ts:39 src/views/config/Config.vue:57
#: src/views/config/ConfigEditor.vue:118 src/views/config/ConfigEditor.vue:79
#: src/routes/index.ts:39 src/views/config/ConfigEditor.vue:118
#: src/views/config/ConfigEditor.vue:79 src/views/config/ConfigList.vue:57
msgid "Dashboard"
msgstr "Bảng điều khiển"
@ -808,6 +808,10 @@ msgstr "Đã bật"
msgid "Encrypt website with 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
msgid "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-"
"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/views/config/ConfigEditor.vue:123 src/views/config/ConfigEditor.vue:84
#: src/routes/index.ts:102 src/views/config/ConfigEditor.vue:123
#: src/views/config/ConfigEditor.vue:84 src/views/config/ConfigList.vue:62
msgid "Manage Configs"
msgstr "Quản lý cấu hình"
@ -1248,6 +1252,7 @@ msgstr "Run Mode"
#: src/components/ChatGPT/ChatGPT.vue:248
#: src/components/StdDesign/StdDataDisplay/StdCurd.vue:181
#: src/components/StdDesign/StdDataDisplay/StdTable.vue:532
#: src/views/config/ConfigList.vue:151
#, fuzzy
msgid "Modify"
msgstr "Sửa"
@ -1718,7 +1723,8 @@ msgstr "Xoá thành công"
msgid "Removed successfully"
msgstr "Xoá thành công"
#: src/views/config/components/Rename.vue:52 src/views/config/Config.vue:130
#: src/views/config/components/Rename.vue:52
#: src/views/config/ConfigList.vue:159
#: src/views/domain/ngx_conf/NgxUpstream.vue:123
#, fuzzy
msgid "Rename"

Binary file not shown.

View file

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

View file

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

View file

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

View file

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

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">
import { message } from 'ant-design-vue'
import type { Ref } from 'vue'
import { InfoCircleOutlined } from '@ant-design/icons-vue'
import { formatDateTime } from '@/lib/helper'
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
import type { Config } from '@/api/config'
import config from '@/api/config'
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
import ngx from '@/api/ngx'
@ -10,7 +12,10 @@ import InspectConfig from '@/views/config/InspectConfig.vue'
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
import type { ChatComplicationMessage } from '@/api/openai'
import { useBreadcrumbs } from '@/composables/useBreadcrumbs'
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
import { useSettingsStore } from '@/pinia'
const settings = useSettingsStore()
const route = useRoute()
const router = useRouter()
const refForm = ref()
@ -32,10 +37,12 @@ const data = ref({
name: '',
content: '',
filepath: '',
})
sync_node_ids: [] as number[],
sync_overwrite: false,
} as Config)
const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]>
const activeKey = ref(['basic', 'chatgpt'])
const activeKey = ref(['basic', 'deploy', 'chatgpt'])
const modifiedAt = ref('')
const nginxConfigBase = ref('')
@ -145,6 +152,8 @@ function save() {
filepath: data.value.filepath,
new_filepath: newPath.value,
content: data.value.content,
sync_node_ids: data.value.sync_node_ids,
sync_overwrite: data.value.sync_overwrite,
}).then(r => {
data.value.content = r.content
message.success($gettext('Saved successfully'))
@ -261,6 +270,29 @@ function goBack() {
</AFormItem>
</AForm>
</ACollapsePanel>
<ACollapsePanel
v-if="!settings.is_remote"
key="deploy"
:header="$gettext('Deploy')"
>
<NodeSelector
v-model:target="data.sync_node_ids"
hidden-local
/>
<div class="node-deploy-control">
<div class="overwrite">
<ACheckbox v-model:checked="data.sync_overwrite">
{{ $gettext('Overwrite') }}
</ACheckbox>
<ATooltip placement="bottom">
<template #title>
{{ $gettext('Overwrite exist file') }}
</template>
<InfoCircleOutlined />
</ATooltip>
</div>
</div>
</ACollapsePanel>
<ACollapsePanel
key="chatgpt"
header="ChatGPT"
@ -295,4 +327,19 @@ function goBack() {
:deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
padding: 0 0 10px 0;
}
.overwrite {
margin-right: 15px;
span {
color: #9b9b9b;
}
}
.node-deploy-control {
display: flex;
justify-content: flex-end;
margin-top: 10px;
align-items: center;
}
</style>

View file

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

View file

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

View file

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

View file

@ -5,6 +5,7 @@ import { FitAddon } from '@xterm/addon-fit'
import _ from 'lodash'
import ws from '@/lib/websocket'
import useOTPModal from '@/components/OTP/useOTPModal'
import otp from '@/api/otp'
let term: Terminal | null
let ping: NodeJS.Timeout
@ -14,10 +15,11 @@ const websocket = shallowRef()
const lostConnection = ref(false)
onMounted(() => {
otp.secure_session_status()
const otpModal = useOTPModal()
otpModal.open({
onOk(secureSessionId: string) {
otpModal.open().then(secureSessionId => {
websocket.value = ws(`/api/pty?X-Secure-Session-ID=${secureSessionId}`, false)
nextTick(() => {
@ -31,13 +33,11 @@ onMounted(() => {
lostConnection.value = true
}
})
},
onCancel() {
}).catch(() => {
if (window.history.length > 1)
router.go(-1)
else
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 (
"github.com/0xJacky/Nginx-UI/settings"
@ -7,7 +7,7 @@ import (
"net/http"
)
func ipWhiteList() gin.HandlerFunc {
func IPWhiteList() gin.HandlerFunc {
return func(c *gin.Context) {
clientIP := c.ClientIP()
if len(settings.AuthSettings.IPWhiteList) == 0 || clientIP == "127.0.0.1" {

View file

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

View file

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

View file

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

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{},
AcmeUser{},
BanIP{},
Config{},
}
}

View file

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

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

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