mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-10 18:05:48 +02:00
feat: deploy config to remote nodes #359
This commit is contained in:
parent
e75dce92ad
commit
1c1da92363
46 changed files with 1480 additions and 605 deletions
|
@ -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(),
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ func Mkdir(c *gin.Context) {
|
|||
api.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "ok",
|
||||
})
|
|
@ -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{
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,9 @@ const otp = {
|
|||
recovery_code,
|
||||
})
|
||||
},
|
||||
secure_session_status() {
|
||||
return http.get('/otp_secure_session_status')
|
||||
},
|
||||
}
|
||||
|
||||
export default otp
|
||||
|
|
18
app/src/components/Notification/cert.ts
Normal file
18
app/src/components/Notification/cert.ts
Normal 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)
|
||||
}
|
37
app/src/components/Notification/config.ts
Normal file
37
app/src/components/Notification/config.ts
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 ""
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
@ -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 "重命名"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1 +1 @@
|
|||
{"version":"2.0.0-beta.28","build_id":147,"total_build":351}
|
||||
{"version":"2.0.0-beta.28","build_id":149,"total_build":353}
|
|
@ -1,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>
|
||||
|
|
|
@ -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
|
|
@ -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}`)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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('/')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
@ -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
292
internal/config/sync.go
Normal 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
|
||||
}
|
|
@ -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" {
|
|
@ -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")
|
|
@ -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 {
|
|
@ -1,4 +1,4 @@
|
|||
package router
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/0xJacky/Nginx-UI/internal/logger"
|
||||
|
@ -9,7 +9,7 @@ import (
|
|||
"net/http"
|
||||
)
|
||||
|
||||
func proxyWs() gin.HandlerFunc {
|
||||
func ProxyWs() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
nodeID, ok := c.Get("ProxyNodeID")
|
||||
if !ok {
|
43
internal/middleware/secure_session.go
Normal file
43
internal/middleware/secure_session.go
Normal 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
9
model/config.go
Normal 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"`
|
||||
}
|
|
@ -36,6 +36,7 @@ func GenerateAllModel() []any {
|
|||
Notification{},
|
||||
AcmeUser{},
|
||||
BanIP{},
|
||||
Config{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
370
query/configs.gen.go
Normal 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
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue