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

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

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{
Name: stat.Name(),
Content: string(content),
ChatGPTMessages: chatgpt.Content,
FilePath: path,
ModifiedAt: stat.ModTime(),
cfg, err := q.Where(q.Filepath.Eq(path)).FirstOrInit()
if err != nil {
api.ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, APIConfigResp{
Config: config.Config{
Name: stat.Name(),
Content: string(content),
ChatGPTMessages: chatgpt.Content,
FilePath: path,
ModifiedAt: stat.ModTime(),
},
SyncNodeIds: cfg.SyncNodeIds,
SyncOverwrite: cfg.SyncOverwrite,
})
}

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"
@ -12,14 +14,16 @@ import (
func Rename(c *gin.Context) {
var json struct {
BasePath string `json:"base_path"`
OrigName string `json:"orig_name"`
NewName string `json:"new_name"`
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
}
// 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() {
// update ChatGPT records
g := query.ChatGPTLog
_, _ = 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,68 +21,72 @@ const useOTPModal = () => {
document.head.appendChild(style)
}
const open = async ({ onOk, onCancel }: OTPModalProps) => {
const open = async (): Promise<string> => {
const { status } = await otp.status()
if (!status) {
onOk?.('')
return
}
return new Promise((resolve, reject) => {
if (!status) {
resolve('')
const cookies = useCookies(['nginx-ui-2fa'])
const ssid = cookies.get('secure_session_id')
if (ssid) {
onOk?.(ssid)
secureSessionId.value = ssid
return
}
return
}
const cookies = useCookies(['nginx-ui-2fa'])
const ssid = cookies.get('secure_session_id')
if (ssid) {
resolve(ssid)
secureSessionId.value = ssid
injectStyles()
let container: HTMLDivElement | null = document.createElement('div')
document.body.appendChild(container)
return
}
const close = () => {
render(null, container!)
document.body.removeChild(container!)
container = null
}
injectStyles()
let container: HTMLDivElement | null = document.createElement('div')
document.body.appendChild(container)
const verify = (passcode: string, recovery: string) => {
otp.start_secure_session(passcode, recovery).then(r => {
cookies.set('secure_session_id', r.session_id, { maxAge: 60 * 3 })
onOk?.(r.session_id)
close()
secureSessionId.value = r.session_id
}).catch(async () => {
refOTPAuthorization.value?.clearInput()
await message.error($gettext('Invalid passcode or recovery code'))
})
}
const close = () => {
render(null, container!)
document.body.removeChild(container!)
container = null
}
const vnode = createVNode(Modal, {
open: true,
title: $gettext('Two-factor authentication required'),
centered: true,
maskClosable: false,
class: randomId,
footer: false,
onCancel: () => {
close()
onCancel?.()
},
}, {
default: () => h(
OTPAuthorization,
{
ref: refOTPAuthorization,
class: 'mt-3',
onOnSubmit: verify,
const verify = (passcode: string, recovery: string) => {
otp.start_secure_session(passcode, recovery).then(r => {
cookies.set('secure_session_id', r.session_id, { maxAge: 60 * 3 })
resolve(r.session_id)
close()
secureSessionId.value = r.session_id
}).catch(async () => {
refOTPAuthorization.value?.clearInput()
await message.error($gettext('Invalid passcode or recovery code'))
})
}
const vnode = createVNode(Modal, {
open: true,
title: $gettext('Two-factor authentication required'),
centered: true,
maskClosable: false,
class: randomId,
footer: false,
onCancel: () => {
close()
// eslint-disable-next-line prefer-promise-reject-errors
reject()
},
),
})
}, {
default: () => h(
OTPAuthorization,
{
ref: refOTPAuthorization,
class: 'mt-3',
onOnSubmit: verify,
},
),
})
render(vnode, container)
render(vnode, container!)
})
}
return { open }

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) {
$router.push({
path: `/config/${basePath}${r}/edit`,
})
}
else {
$router.push({
query: {
dir: basePath + r,
},
})
}
}"
disable-modify
>
<template #actions="{ record }">
<AButton
type="link"
size="small"
@click="() => {
if (!record.is_dir) {
$router.push({
path: `/config/${basePath}${record.name}/edit`,
})
}
else {
$router.push({
query: {
dir: basePath + record.name,
},
})
}
}"
>
{{ $gettext('Modify') }}
</AButton>
<ADivider type="vertical" />
<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,17 +27,15 @@ function ok() {
refForm.value.validate().then(() => {
const otpModal = useOTPModal()
otpModal.open({
onOk() {
config.mkdir(data.value.basePath, data.value.name).then(() => {
visible.value = false
otpModal.open().then(() => {
config.mkdir(data.value.basePath, data.value.name).then(() => {
visible.value = false
message.success($gettext('Created successfully'))
emit('created')
}).catch(e => {
message.error(`${$gettext('Server error')} ${e?.message}`)
})
},
message.success($gettext('Created successfully'))
emit('created')
}).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(() => {
visible.value = false
message.success($gettext('Rename successfully'))
emit('renamed')
}).catch(e => {
message.error(`${$gettext('Server error')} ${e?.message}`)
})
},
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,30 +15,29 @@ const websocket = shallowRef()
const lostConnection = ref(false)
onMounted(() => {
otp.secure_session_status()
const otpModal = useOTPModal()
otpModal.open({
onOk(secureSessionId: string) {
websocket.value = ws(`/api/pty?X-Secure-Session-ID=${secureSessionId}`, false)
otpModal.open().then(secureSessionId => {
websocket.value = ws(`/api/pty?X-Secure-Session-ID=${secureSessionId}`, false)
nextTick(() => {
initTerm()
websocket.value.onmessage = wsOnMessage
websocket.value.onopen = wsOnOpen
websocket.value.onerror = () => {
lostConnection.value = true
}
websocket.value.onclose = () => {
lostConnection.value = true
}
})
},
onCancel() {
if (window.history.length > 1)
router.go(-1)
else
router.push('/')
},
nextTick(() => {
initTerm()
websocket.value.onmessage = wsOnMessage
websocket.value.onopen = wsOnOpen
websocket.value.onerror = () => {
lostConnection.value = true
}
websocket.value.onclose = () => {
lostConnection.value = true
}
})
}).catch(() => {
if (window.history.length > 1)
router.go(-1)
else
router.push('/')
})
})

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

370
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

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