mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 02:15:48 +02:00
feat(site): sync operation
This commit is contained in:
parent
6c137e5229
commit
22e37e4b61
43 changed files with 4875 additions and 3712 deletions
|
@ -2,15 +2,14 @@ package sites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/0xJacky/Nginx-UI/api"
|
"github.com/0xJacky/Nginx-UI/api"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/helper"
|
"github.com/0xJacky/Nginx-UI/internal/site"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func DuplicateSite(c *gin.Context) {
|
func DuplicateSite(c *gin.Context) {
|
||||||
// Source name
|
// Source name
|
||||||
name := c.Param("name")
|
src := c.Param("name")
|
||||||
|
|
||||||
// Destination name
|
// Destination name
|
||||||
var json struct {
|
var json struct {
|
||||||
|
@ -21,24 +20,13 @@ func DuplicateSite(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
src := nginx.GetConfPath("sites-available", name)
|
err := site.Duplicate(src, json.Name)
|
||||||
dst := nginx.GetConfPath("sites-available", json.Name)
|
|
||||||
|
|
||||||
if helper.FileExists(dst) {
|
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{
|
|
||||||
"message": "File exists",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := helper.CopyFile(src, dst)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"dst": dst,
|
"message": "ok",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,17 +3,25 @@ package sites
|
||||||
import "github.com/gin-gonic/gin"
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
func InitRouter(r *gin.RouterGroup) {
|
func InitRouter(r *gin.RouterGroup) {
|
||||||
r.GET("domains", GetSiteList)
|
r.GET("sites", GetSiteList)
|
||||||
r.GET("domains/:name", GetSite)
|
r.GET("sites/:name", GetSite)
|
||||||
r.POST("domains/:name", SaveSite)
|
r.PUT("sites", BatchUpdateSites)
|
||||||
r.PUT("domains", BatchUpdateSites)
|
r.POST("sites/:name/advance", DomainEditByAdvancedMode)
|
||||||
r.POST("domains/:name/enable", EnableSite)
|
|
||||||
r.POST("domains/:name/disable", DisableSite)
|
|
||||||
r.POST("domains/:name/advance", DomainEditByAdvancedMode)
|
|
||||||
r.DELETE("domains/:name", DeleteSite)
|
|
||||||
r.POST("domains/:name/duplicate", DuplicateSite)
|
|
||||||
r.POST("auto_cert/:name", AddDomainToAutoCert)
|
r.POST("auto_cert/:name", AddDomainToAutoCert)
|
||||||
r.DELETE("auto_cert/:name", RemoveDomainFromAutoCert)
|
r.DELETE("auto_cert/:name", RemoveDomainFromAutoCert)
|
||||||
|
|
||||||
|
// rename site
|
||||||
|
r.POST("sites/:name/rename", RenameSite)
|
||||||
|
// enable site
|
||||||
|
r.POST("sites/:name/enable", EnableSite)
|
||||||
|
// disable site
|
||||||
|
r.POST("sites/:name/disable", DisableSite)
|
||||||
|
// save site
|
||||||
|
r.POST("sites/:name", SaveSite)
|
||||||
|
// delete site
|
||||||
|
r.DELETE("sites/:name", DeleteSite)
|
||||||
|
// duplicate site
|
||||||
|
r.POST("sites/:name/duplicate", DuplicateSite)
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitCategoryRouter(r *gin.RouterGroup) {
|
func InitCategoryRouter(r *gin.RouterGroup) {
|
||||||
|
|
|
@ -3,8 +3,8 @@ package sites
|
||||||
import (
|
import (
|
||||||
"github.com/0xJacky/Nginx-UI/api"
|
"github.com/0xJacky/Nginx-UI/api"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/cert"
|
"github.com/0xJacky/Nginx-UI/internal/cert"
|
||||||
"github.com/0xJacky/Nginx-UI/internal/helper"
|
|
||||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/site"
|
||||||
"github.com/0xJacky/Nginx-UI/model"
|
"github.com/0xJacky/Nginx-UI/model"
|
||||||
"github.com/0xJacky/Nginx-UI/query"
|
"github.com/0xJacky/Nginx-UI/query"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
@ -17,14 +17,8 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetSite(c *gin.Context) {
|
func GetSite(c *gin.Context) {
|
||||||
rewriteName, ok := c.Get("rewriteConfigFileName")
|
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
|
|
||||||
// for modify filename
|
|
||||||
if ok {
|
|
||||||
name = rewriteName.(string)
|
|
||||||
}
|
|
||||||
|
|
||||||
path := nginx.GetConfPath("sites-available", name)
|
path := nginx.GetConfPath("sites-available", name)
|
||||||
file, err := os.Stat(path)
|
file, err := os.Stat(path)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
|
@ -51,7 +45,7 @@ func GetSite(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
s := query.Site
|
s := query.Site
|
||||||
site, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
|
siteModel, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
return
|
return
|
||||||
|
@ -62,7 +56,7 @@ func GetSite(c *gin.Context) {
|
||||||
logger.Warn(err)
|
logger.Warn(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if site.Advanced {
|
if siteModel.Advanced {
|
||||||
origContent, err := os.ReadFile(path)
|
origContent, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
|
@ -71,7 +65,7 @@ func GetSite(c *gin.Context) {
|
||||||
|
|
||||||
c.JSON(http.StatusOK, Site{
|
c.JSON(http.StatusOK, Site{
|
||||||
ModifiedAt: file.ModTime(),
|
ModifiedAt: file.ModTime(),
|
||||||
Site: site,
|
Site: siteModel,
|
||||||
Enabled: enabled,
|
Enabled: enabled,
|
||||||
Name: name,
|
Name: name,
|
||||||
Config: string(origContent),
|
Config: string(origContent),
|
||||||
|
@ -103,8 +97,8 @@ func GetSite(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, Site{
|
c.JSON(http.StatusOK, Site{
|
||||||
|
Site: siteModel,
|
||||||
ModifiedAt: file.ModTime(),
|
ModifiedAt: file.ModTime(),
|
||||||
Site: site,
|
|
||||||
Enabled: enabled,
|
Enabled: enabled,
|
||||||
Name: name,
|
Name: name,
|
||||||
Config: nginxConfig.FmtCode(),
|
Config: nginxConfig.FmtCode(),
|
||||||
|
@ -119,15 +113,7 @@ func GetSite(c *gin.Context) {
|
||||||
func SaveSite(c *gin.Context) {
|
func SaveSite(c *gin.Context) {
|
||||||
name := c.Param("name")
|
name := c.Param("name")
|
||||||
|
|
||||||
if name == "" {
|
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{
|
|
||||||
"message": "param name is empty",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var json struct {
|
var json struct {
|
||||||
Name string `json:"name" binding:"required"`
|
|
||||||
Content string `json:"content" binding:"required"`
|
Content string `json:"content" binding:"required"`
|
||||||
SiteCategoryID uint64 `json:"site_category_id"`
|
SiteCategoryID uint64 `json:"site_category_id"`
|
||||||
SyncNodeIDs []uint64 `json:"sync_node_ids"`
|
SyncNodeIDs []uint64 `json:"sync_node_ids"`
|
||||||
|
@ -138,129 +124,39 @@ func SaveSite(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
path := nginx.GetConfPath("sites-available", name)
|
err := site.Save(name, json.Content, json.Overwrite, json.SiteCategoryID, json.SyncNodeIDs)
|
||||||
|
|
||||||
if !json.Overwrite && helper.FileExists(path) {
|
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{
|
|
||||||
"message": "File exists",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err := os.WriteFile(path, []byte(json.Content), 0644)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
|
|
||||||
s := query.Site
|
|
||||||
|
|
||||||
_, err = s.Where(s.Path.Eq(path)).
|
|
||||||
Select(s.SiteCategoryID, s.SyncNodeIDs).
|
|
||||||
Updates(&model.Site{
|
|
||||||
SiteCategoryID: json.SiteCategoryID,
|
|
||||||
SyncNodeIDs: json.SyncNodeIDs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
api.ErrHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// rename the config file if needed
|
|
||||||
if name != json.Name {
|
|
||||||
newPath := nginx.GetConfPath("sites-available", json.Name)
|
|
||||||
_, _ = s.Where(s.Path.Eq(path)).Update(s.Path, newPath)
|
|
||||||
|
|
||||||
// check if dst file exists, do not rename
|
|
||||||
if helper.FileExists(newPath) {
|
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{
|
|
||||||
"message": "File exists",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// recreate a soft link
|
|
||||||
if helper.FileExists(enabledConfigFilePath) {
|
|
||||||
_ = os.Remove(enabledConfigFilePath)
|
|
||||||
enabledConfigFilePath = nginx.GetConfPath("sites-enabled", json.Name)
|
|
||||||
err = os.Symlink(newPath, enabledConfigFilePath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
api.ErrHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Rename(path, newPath)
|
|
||||||
if err != nil {
|
|
||||||
api.ErrHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
name = json.Name
|
|
||||||
c.Set("rewriteConfigFileName", name)
|
|
||||||
}
|
|
||||||
|
|
||||||
enabledConfigFilePath = nginx.GetConfPath("sites-enabled", name)
|
|
||||||
if helper.FileExists(enabledConfigFilePath) {
|
|
||||||
// Test nginx configuration
|
|
||||||
output := nginx.TestConf()
|
|
||||||
|
|
||||||
if nginx.GetLogLevel(output) > nginx.Warn {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
|
||||||
"message": output,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
output = nginx.Reload()
|
|
||||||
|
|
||||||
if nginx.GetLogLevel(output) > nginx.Warn {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
|
||||||
"message": output,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
GetSite(c)
|
GetSite(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnableSite(c *gin.Context) {
|
func RenameSite(c *gin.Context) {
|
||||||
configFilePath := nginx.GetConfPath("sites-available", c.Param("name"))
|
oldName := c.Param("name")
|
||||||
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
|
var json struct {
|
||||||
|
NewName string `json:"new_name"`
|
||||||
_, err := os.Stat(configFilePath)
|
}
|
||||||
|
if !cosy.BindAndValid(c, &json) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := site.Rename(oldName, json.NewName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
|
c.JSON(http.StatusOK, gin.H{
|
||||||
err = os.Symlink(configFilePath, enabledConfigFilePath)
|
"message": "ok",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if err != nil {
|
func EnableSite(c *gin.Context) {
|
||||||
api.ErrHandler(c, err)
|
err := site.Enable(c.Param("name"))
|
||||||
return
|
if err != nil {
|
||||||
}
|
api.ErrHandler(c, err)
|
||||||
}
|
|
||||||
|
|
||||||
// Test nginx config, if not pass, then disable the site.
|
|
||||||
output := nginx.TestConf()
|
|
||||||
if nginx.GetLogLevel(output) > nginx.Warn {
|
|
||||||
_ = os.Remove(enabledConfigFilePath)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
|
||||||
"message": output,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
output = nginx.Reload()
|
|
||||||
|
|
||||||
if nginx.GetLogLevel(output) > nginx.Warn {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
|
||||||
"message": output,
|
|
||||||
})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,71 +166,19 @@ func EnableSite(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func DisableSite(c *gin.Context) {
|
func DisableSite(c *gin.Context) {
|
||||||
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
|
err := site.Disable(c.Param("name"))
|
||||||
_, err := os.Stat(enabledConfigFilePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = os.Remove(enabledConfigFilePath)
|
|
||||||
if err != nil {
|
|
||||||
api.ErrHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete auto cert record
|
|
||||||
certModel := model.Cert{Filename: c.Param("name")}
|
|
||||||
err = certModel.Remove()
|
|
||||||
if err != nil {
|
|
||||||
api.ErrHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
output := nginx.Reload()
|
|
||||||
if nginx.GetLogLevel(output) > nginx.Warn {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{
|
|
||||||
"message": output,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"message": "ok",
|
"message": "ok",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteSite(c *gin.Context) {
|
func DeleteSite(c *gin.Context) {
|
||||||
var err error
|
err := site.Delete(c.Param("name"))
|
||||||
name := c.Param("name")
|
|
||||||
availablePath := nginx.GetConfPath("sites-available", name)
|
|
||||||
|
|
||||||
s := query.Site
|
|
||||||
_, err = s.Where(s.Path.Eq(availablePath)).Unscoped().Delete(&model.Site{})
|
|
||||||
if err != nil {
|
|
||||||
api.ErrHandler(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
enabledPath := nginx.GetConfPath("sites-enabled", name)
|
|
||||||
if _, err = os.Stat(availablePath); os.IsNotExist(err) {
|
|
||||||
c.JSON(http.StatusNotFound, gin.H{
|
|
||||||
"message": "site not found",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err = os.Stat(enabledPath); err == nil {
|
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{
|
|
||||||
"message": "site is enabled",
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
certModel := model.Cert{Filename: name}
|
|
||||||
_ = certModel.Remove()
|
|
||||||
|
|
||||||
err = os.Remove(availablePath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
api.ErrHandler(c, err)
|
api.ErrHandler(c, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -29,7 +29,7 @@ export interface AutoCertRequest {
|
||||||
key_type: PrivateKeyType
|
key_type: PrivateKeyType
|
||||||
}
|
}
|
||||||
|
|
||||||
class Domain extends Curd<Site> {
|
class SiteCurd extends Curd<Site> {
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
enable(name: string, config?: any) {
|
enable(name: string, config?: any) {
|
||||||
return http.post(`${this.baseUrl}/${name}/enable`, undefined, config)
|
return http.post(`${this.baseUrl}/${name}/enable`, undefined, config)
|
||||||
|
@ -39,6 +39,10 @@ class Domain extends Curd<Site> {
|
||||||
return http.post(`${this.baseUrl}/${name}/disable`)
|
return http.post(`${this.baseUrl}/${name}/disable`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rename(oldName: string, newName: string) {
|
||||||
|
return http.post(`${this.baseUrl}/${oldName}/rename`, { new_name: newName })
|
||||||
|
}
|
||||||
|
|
||||||
get_template() {
|
get_template() {
|
||||||
return http.get('template')
|
return http.get('template')
|
||||||
}
|
}
|
||||||
|
@ -60,6 +64,6 @@ class Domain extends Curd<Site> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const domain = new Domain('/domains')
|
const site = new SiteCurd('/sites')
|
||||||
|
|
||||||
export default domain
|
export default site
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import type { Column, JSXElements, StdDesignEdit } from '@/components/StdDesign/types'
|
import type { Column, JSXElements, StdDesignEdit } from '@/components/StdDesign/types'
|
||||||
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { labelRender } from '@/components/StdDesign/StdDataEntry'
|
import { labelRender } from '@/components/StdDesign/StdDataEntry'
|
||||||
import StdFormItem from '@/components/StdDesign/StdDataEntry/StdFormItem.vue'
|
import StdFormItem from '@/components/StdDesign/StdDataEntry/StdFormItem.vue'
|
||||||
|
@ -7,26 +8,13 @@ import { Form } from 'ant-design-vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
dataList: Column[]
|
dataList: Column[]
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
|
||||||
dataSource: Record<string, any>
|
|
||||||
errors?: Record<string, string>
|
errors?: Record<string, string>
|
||||||
type?: 'search' | 'edit'
|
type?: 'search' | 'edit'
|
||||||
layout?: 'horizontal' | 'vertical' | 'inline'
|
layout?: 'horizontal' | 'vertical' | 'inline'
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
const dataSource = defineModel<Record<string, any>>('dataSource')
|
||||||
'update:dataSource': [data: Record<string, any>]
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const dataSource = computed({
|
|
||||||
get() {
|
|
||||||
return props.dataSource
|
|
||||||
},
|
|
||||||
set(v) {
|
|
||||||
emit('update:dataSource', v)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|
||||||
|
@ -37,7 +25,7 @@ function extraRender(extra?: string | (() => string)) {
|
||||||
return extra
|
return extra
|
||||||
}
|
}
|
||||||
|
|
||||||
const formRef = ref<InstanceType<typeof Form>>()
|
const formRef = ref<FormInstance>()
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
formRef,
|
formRef,
|
||||||
|
@ -50,7 +38,7 @@ function Render() {
|
||||||
props.dataList.forEach((v: Column) => {
|
props.dataList.forEach((v: Column) => {
|
||||||
const dataIndex = (v.edit?.actualDataIndex ?? v.dataIndex) as string
|
const dataIndex = (v.edit?.actualDataIndex ?? v.dataIndex) as string
|
||||||
|
|
||||||
dataSource.value[dataIndex] = props.dataSource[dataIndex]
|
dataSource.value![dataIndex] = dataSource.value![dataIndex]
|
||||||
if (props.type === 'search') {
|
if (props.type === 'search') {
|
||||||
if (v.search) {
|
if (v.search) {
|
||||||
const type = (v.search as StdDesignEdit)?.type || v.edit?.type
|
const type = (v.search as StdDesignEdit)?.type || v.edit?.type
|
||||||
|
@ -75,7 +63,7 @@ function Render() {
|
||||||
|
|
||||||
let show = true
|
let show = true
|
||||||
if (v.edit?.show && typeof v.edit.show === 'function')
|
if (v.edit?.show && typeof v.edit.show === 'function')
|
||||||
show = v.edit.show(props.dataSource)
|
show = v.edit.show(dataSource.value)
|
||||||
|
|
||||||
if (v.edit?.type && show) {
|
if (v.edit?.type && show) {
|
||||||
template.push(
|
template.push(
|
||||||
|
@ -87,6 +75,7 @@ function Render() {
|
||||||
error={props.errors}
|
error={props.errors}
|
||||||
required={v.edit?.config?.required}
|
required={v.edit?.config?.required}
|
||||||
hint={v.edit?.hint}
|
hint={v.edit?.hint}
|
||||||
|
noValidate={v.edit?.config?.noValidate}
|
||||||
>
|
>
|
||||||
{v.edit.type(v.edit, dataSource.value, dataIndex)}
|
{v.edit.type(v.edit, dataSource.value, dataIndex)}
|
||||||
</StdFormItem>,
|
</StdFormItem>,
|
||||||
|
@ -97,7 +86,16 @@ function Render() {
|
||||||
if (slots.action)
|
if (slots.action)
|
||||||
template.push(<div class="std-data-entry-action">{slots.action()}</div>)
|
template.push(<div class="std-data-entry-action">{slots.action()}</div>)
|
||||||
|
|
||||||
return <Form ref={formRef} model={dataSource.value} layout={props.layout || 'vertical'}>{template}</Form>
|
return (
|
||||||
|
<Form
|
||||||
|
class="my-10px!"
|
||||||
|
ref={formRef}
|
||||||
|
model={dataSource.value}
|
||||||
|
layout={props.layout || 'vertical'}
|
||||||
|
>
|
||||||
|
{template}
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Column } from '@/components/StdDesign/types'
|
import type { Column } from '@/components/StdDesign/types'
|
||||||
import { computed } from 'vue'
|
import type { Rule } from 'ant-design-vue/es/form'
|
||||||
|
import FormErrors from '@/constants/form_errors'
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
@ -13,18 +14,42 @@ export interface Props {
|
||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
required?: boolean
|
required?: boolean
|
||||||
|
noValidate?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const tag = computed(() => {
|
const tag = computed(() => {
|
||||||
return props.error?.[props.dataIndex!.toString()] ?? ''
|
return props.error?.[props.dataIndex!.toString()] ?? ''
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// const valid_status = computed(() => {
|
||||||
|
// if (tag.value)
|
||||||
|
// return 'error'
|
||||||
|
// else return 'success'
|
||||||
|
// })
|
||||||
|
|
||||||
const help = computed(() => {
|
const help = computed(() => {
|
||||||
if (tag.value.includes('required'))
|
const rules = tag.value.split(',')
|
||||||
return $gettext('This field should not be empty')
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (FormErrors[rule])
|
||||||
|
return FormErrors[rule]()
|
||||||
|
}
|
||||||
|
|
||||||
return props.hint
|
return props.hint
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
|
async function validator(_: Rule, value: any): Promise<any> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (props.required && !props.noValidate && (!value && value !== 0)) {
|
||||||
|
reject(help.value ?? $gettext('This field should not be empty'))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -32,7 +57,9 @@ const help = computed(() => {
|
||||||
:name="dataIndex as string"
|
:name="dataIndex as string"
|
||||||
:label="label"
|
:label="label"
|
||||||
:help="help"
|
:help="help"
|
||||||
:required="required"
|
:rules="{ required, validator }"
|
||||||
|
:validate-status="tag ? 'error' : undefined"
|
||||||
|
:auto-link="false"
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</AFormItem>
|
</AFormItem>
|
||||||
|
|
20
app/src/components/StdDesign/types.d.ts
vendored
20
app/src/components/StdDesign/types.d.ts
vendored
|
@ -45,19 +45,29 @@ export interface StdDesignEdit {
|
||||||
|
|
||||||
config?: {
|
config?: {
|
||||||
label?: string | (() => string) // label for form item
|
label?: string | (() => string) // label for form item
|
||||||
size?: string // class size of Std image upload
|
recordValueIndex?: any // relative to api return
|
||||||
placeholder?: string | (() => string) // placeholder for input
|
placeholder?: string | (() => string) // placeholder for input
|
||||||
generate?: boolean // generate btn for StdPassword
|
generate?: boolean // generate btn for StdPassword
|
||||||
|
selectionType?: any
|
||||||
|
api?: Curd
|
||||||
|
valueApi?: Curd
|
||||||
|
columns?: any
|
||||||
|
disableSearch?: boolean
|
||||||
|
description?: string
|
||||||
|
bind?: any
|
||||||
|
itemKey?: any // default is id
|
||||||
|
dataSourceValueIndex?: any // relative to dataSource
|
||||||
|
defaultValue?: any
|
||||||
|
required?: boolean
|
||||||
|
noValidate?: boolean
|
||||||
min?: number // min value for input number
|
min?: number // min value for input number
|
||||||
max?: number // max value for input number
|
max?: number // max value for input number
|
||||||
error_messages?: Ref
|
|
||||||
required?: boolean
|
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
|
||||||
defaultValue?: any
|
|
||||||
addonBefore?: string // for inputNumber
|
addonBefore?: string // for inputNumber
|
||||||
addonAfter?: string // for inputNumber
|
addonAfter?: string // for inputNumber
|
||||||
prefix?: string // for inputNumber
|
prefix?: string // for inputNumber
|
||||||
suffix?: string // for inputNumber
|
suffix?: string // for inputNumber
|
||||||
|
size?: string // class size of Std image upload
|
||||||
|
error_messages?: Ref
|
||||||
}
|
}
|
||||||
|
|
||||||
flex?: Flex
|
flex?: Flex
|
||||||
|
|
7
app/src/constants/form_errors.ts
Normal file
7
app/src/constants/form_errors.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
required: () => $gettext('This field should not be empty'),
|
||||||
|
email: () => $gettext('This field should be a valid email address'),
|
||||||
|
db_unique: () => $gettext('This value is already taken'),
|
||||||
|
hostname: () => $gettext('This field should be a valid hostname'),
|
||||||
|
safety_text: () => $gettext('This field should only contain letters, unicode characters, numbers, and -_.'),
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
Binary file not shown.
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,9 +1,9 @@
|
||||||
import type { AxiosRequestConfig } from 'axios'
|
import type { AxiosRequestConfig } from 'axios'
|
||||||
import use2FAModal from '@/components/TwoFA/use2FAModal'
|
import use2FAModal from '@/components/TwoFA/use2FAModal'
|
||||||
|
import { useNProgress } from '@/lib/nprogress/nprogress'
|
||||||
import { useSettingsStore, useUserStore } from '@/pinia'
|
import { useSettingsStore, useUserStore } from '@/pinia'
|
||||||
import router from '@/routes'
|
import router from '@/routes'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import NProgress from 'nprogress'
|
|
||||||
|
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import 'nprogress/nprogress.css'
|
import 'nprogress/nprogress.css'
|
||||||
|
@ -26,9 +26,11 @@ const instance = axios.create({
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const nprogress = useNProgress()
|
||||||
|
|
||||||
instance.interceptors.request.use(
|
instance.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
NProgress.start()
|
nprogress.start()
|
||||||
if (token.value) {
|
if (token.value) {
|
||||||
// eslint-disable-next-line ts/no-explicit-any
|
// eslint-disable-next-line ts/no-explicit-any
|
||||||
(config.headers as any).Authorization = token.value
|
(config.headers as any).Authorization = token.value
|
||||||
|
@ -53,12 +55,12 @@ instance.interceptors.request.use(
|
||||||
|
|
||||||
instance.interceptors.response.use(
|
instance.interceptors.response.use(
|
||||||
response => {
|
response => {
|
||||||
NProgress.done()
|
nprogress.done()
|
||||||
|
|
||||||
return Promise.resolve(response.data)
|
return Promise.resolve(response.data)
|
||||||
},
|
},
|
||||||
async error => {
|
async error => {
|
||||||
NProgress.done()
|
nprogress.done()
|
||||||
|
|
||||||
const otpModal = use2FAModal()
|
const otpModal = use2FAModal()
|
||||||
switch (error.response.status) {
|
switch (error.response.status) {
|
||||||
|
|
16
app/src/lib/nprogress/nprogress.ts
Normal file
16
app/src/lib/nprogress/nprogress.ts
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import _ from 'lodash'
|
||||||
|
import NProgress from 'nprogress'
|
||||||
|
|
||||||
|
NProgress.configure({ showSpinner: false, trickleSpeed: 300 })
|
||||||
|
|
||||||
|
const done = _.debounce(NProgress.done, 300, {
|
||||||
|
leading: false,
|
||||||
|
trailing: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useNProgress() {
|
||||||
|
return {
|
||||||
|
start: NProgress.start,
|
||||||
|
done,
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import type { RouteRecordRaw } from 'vue-router'
|
import type { RouteRecordRaw } from 'vue-router'
|
||||||
import { useSettingsStore, useUserStore } from '@/pinia'
|
import { useNProgress } from '@/lib/nprogress/nprogress'
|
||||||
|
|
||||||
|
import { useSettingsStore, useUserStore } from '@/pinia'
|
||||||
import {
|
import {
|
||||||
BellOutlined,
|
BellOutlined,
|
||||||
CloudOutlined,
|
CloudOutlined,
|
||||||
|
@ -15,10 +16,8 @@ import {
|
||||||
ShareAltOutlined,
|
ShareAltOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
} from '@ant-design/icons-vue'
|
} from '@ant-design/icons-vue'
|
||||||
import NProgress from 'nprogress'
|
|
||||||
|
|
||||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||||
|
|
||||||
import 'nprogress/nprogress.css'
|
import 'nprogress/nprogress.css'
|
||||||
|
|
||||||
export const routes: RouteRecordRaw[] = [
|
export const routes: RouteRecordRaw[] = [
|
||||||
|
@ -74,7 +73,7 @@ export const routes: RouteRecordRaw[] = [
|
||||||
}, {
|
}, {
|
||||||
path: ':name',
|
path: ':name',
|
||||||
name: 'Edit Site',
|
name: 'Edit Site',
|
||||||
component: () => import('@/views/site/SiteEdit.vue'),
|
component: () => import('@/views/site/site_edit/SiteEdit.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
name: () => $gettext('Edit Site'),
|
name: () => $gettext('Edit Site'),
|
||||||
hiddenInSidebar: true,
|
hiddenInSidebar: true,
|
||||||
|
@ -324,12 +323,12 @@ const router = createRouter({
|
||||||
routes,
|
routes,
|
||||||
})
|
})
|
||||||
|
|
||||||
NProgress.configure({ showSpinner: false })
|
const nprogress = useNProgress()
|
||||||
|
|
||||||
router.beforeEach((to, _, next) => {
|
router.beforeEach((to, _, next) => {
|
||||||
document.title = `${to?.meta.name?.() ?? ''} | Nginx UI`
|
document.title = `${to?.meta.name?.() ?? ''} | Nginx UI`
|
||||||
|
|
||||||
NProgress.start()
|
nprogress.start()
|
||||||
|
|
||||||
const user = useUserStore()
|
const user = useUserStore()
|
||||||
|
|
||||||
|
@ -340,7 +339,7 @@ router.beforeEach((to, _, next) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
router.afterEach(() => {
|
router.afterEach(() => {
|
||||||
NProgress.done()
|
nprogress.done()
|
||||||
})
|
})
|
||||||
|
|
||||||
export default router
|
export default router
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { NgxConfig } from '@/api/ngx'
|
import type { NgxConfig } from '@/api/ngx'
|
||||||
import domain from '@/api/domain'
|
|
||||||
import ngx from '@/api/ngx'
|
import ngx from '@/api/ngx'
|
||||||
|
import site from '@/api/site'
|
||||||
import DirectiveEditor from '@/views/site/ngx_conf/directive/DirectiveEditor.vue'
|
import DirectiveEditor from '@/views/site/ngx_conf/directive/DirectiveEditor.vue'
|
||||||
import LocationEditor from '@/views/site/ngx_conf/LocationEditor.vue'
|
import LocationEditor from '@/views/site/ngx_conf/LocationEditor.vue'
|
||||||
import NgxConfigEditor from '@/views/site/ngx_conf/NgxConfigEditor.vue'
|
import NgxConfigEditor from '@/views/site/ngx_conf/NgxConfigEditor.vue'
|
||||||
|
@ -26,17 +26,17 @@ onMounted(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
domain.get_template().then(r => {
|
site.get_template().then(r => {
|
||||||
Object.assign(ngx_config, r.tokenized)
|
Object.assign(ngx_config, r.tokenized)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function save() {
|
async function save() {
|
||||||
return ngx.build_config(ngx_config).then(r => {
|
return ngx.build_config(ngx_config).then(r => {
|
||||||
domain.save(ngx_config.name, { name: ngx_config.name, content: r.content, overwrite: true }).then(() => {
|
site.save(ngx_config.name, { name: ngx_config.name, content: r.content, overwrite: true }).then(() => {
|
||||||
message.success($gettext('Saved successfully'))
|
message.success($gettext('Saved successfully'))
|
||||||
|
|
||||||
domain.enable(ngx_config.name).then(() => {
|
site.enable(ngx_config.name).then(() => {
|
||||||
message.success($gettext('Enabled successfully'))
|
message.success($gettext('Enabled successfully'))
|
||||||
window.scroll({ top: 0, left: 0, behavior: 'smooth' })
|
window.scroll({ top: 0, left: 0, behavior: 'smooth' })
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
|
|
|
@ -4,7 +4,7 @@ import type { CertificateResult } from '@/api/cert'
|
||||||
import type { NgxConfig, NgxDirective } from '@/api/ngx'
|
import type { NgxConfig, NgxDirective } from '@/api/ngx'
|
||||||
import type { PrivateKeyType } from '@/constants'
|
import type { PrivateKeyType } from '@/constants'
|
||||||
import type { ComputedRef, Ref } from 'vue'
|
import type { ComputedRef, Ref } from 'vue'
|
||||||
import domain from '@/api/domain'
|
import site from '@/api/site'
|
||||||
import AutoCertStepOne from '@/views/site/cert/components/AutoCertStepOne.vue'
|
import AutoCertStepOne from '@/views/site/cert/components/AutoCertStepOne.vue'
|
||||||
import ObtainCertLive from '@/views/site/cert/components/ObtainCertLive.vue'
|
import ObtainCertLive from '@/views/site/cert/components/ObtainCertLive.vue'
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
|
@ -59,7 +59,7 @@ async function resolveCert({ ssl_certificate, ssl_certificate_key, key_type }: C
|
||||||
|
|
||||||
function change_auto_cert(status: boolean, key_type?: PrivateKeyType) {
|
function change_auto_cert(status: boolean, key_type?: PrivateKeyType) {
|
||||||
if (status) {
|
if (status) {
|
||||||
domain.add_auto_cert(props.configName, {
|
site.add_auto_cert(props.configName, {
|
||||||
domains: name.value.trim().split(' '),
|
domains: name.value.trim().split(' '),
|
||||||
challenge_method: data.value.challenge_method!,
|
challenge_method: data.value.challenge_method!,
|
||||||
dns_credential_id: data.value.dns_credential_id!,
|
dns_credential_id: data.value.dns_credential_id!,
|
||||||
|
@ -71,7 +71,7 @@ function change_auto_cert(status: boolean, key_type?: PrivateKeyType) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
domain.remove_auto_cert(props.configName).then(() => {
|
site.remove_auto_cert(props.configName).then(() => {
|
||||||
message.success($gettext('Auto-renewal disabled for %{name}', { name: name.value }))
|
message.success($gettext('Auto-renewal disabled for %{name}', { name: name.value }))
|
||||||
}).catch(e => {
|
}).catch(e => {
|
||||||
message.error(e.message ?? $gettext('Disable auto-renewal failed for %{name}', { name: name.value }))
|
message.error(e.message ?? $gettext('Disable auto-renewal failed for %{name}', { name: name.value }))
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
|
|
||||||
|
|
||||||
const node_map = ref({})
|
|
||||||
const target = ref([])
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<NodeSelector
|
|
||||||
v-model:target="target"
|
|
||||||
v-model:map="node_map"
|
|
||||||
class="mb-4"
|
|
||||||
hidden-local
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped lang="less">
|
|
||||||
.overwrite {
|
|
||||||
margin-right: 15px;
|
|
||||||
|
|
||||||
span {
|
|
||||||
color: #9b9b9b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.node-deploy-control {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: 10px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,149 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import domain from '@/api/domain'
|
|
||||||
|
|
||||||
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
|
|
||||||
import gettext from '@/gettext'
|
|
||||||
import { useSettingsStore } from '@/pinia'
|
|
||||||
import { Form, message, notification } from 'ant-design-vue'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
visible: boolean
|
|
||||||
name: string
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const emit = defineEmits(['update:visible', 'duplicated'])
|
|
||||||
|
|
||||||
const settings = useSettingsStore()
|
|
||||||
|
|
||||||
const show = computed({
|
|
||||||
get() {
|
|
||||||
return props.visible
|
|
||||||
},
|
|
||||||
set(v) {
|
|
||||||
emit('update:visible', v)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
interface Model {
|
|
||||||
name: string // site name
|
|
||||||
target: number[] // ids of deploy targets
|
|
||||||
}
|
|
||||||
|
|
||||||
const modelRef: Model = reactive({ name: '', target: [] })
|
|
||||||
|
|
||||||
const rulesRef = reactive({
|
|
||||||
name: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: () => $gettext('Please input name, '
|
|
||||||
+ 'this will be used as the filename of the new configuration!'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
target: [
|
|
||||||
{
|
|
||||||
required: true,
|
|
||||||
message: () => $gettext('Please select at least one node!'),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
const { validate, validateInfos, clearValidate } = Form.useForm(modelRef, rulesRef)
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
|
|
||||||
const node_map: Record<number, string> = reactive({})
|
|
||||||
|
|
||||||
function onSubmit() {
|
|
||||||
validate().then(async () => {
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
modelRef.target.forEach(id => {
|
|
||||||
if (id === 0) {
|
|
||||||
domain.duplicate(props.name, { name: modelRef.name }).then(() => {
|
|
||||||
message.success($gettext('Duplicate to local successfully'))
|
|
||||||
show.value = false
|
|
||||||
emit('duplicated')
|
|
||||||
}).catch(e => {
|
|
||||||
message.error($gettext(e?.message ?? 'Server error'))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// get source content
|
|
||||||
|
|
||||||
domain.get(props.name).then(r => {
|
|
||||||
domain.save(modelRef.name, {
|
|
||||||
name: modelRef.name,
|
|
||||||
content: r.config,
|
|
||||||
|
|
||||||
}, { headers: { 'X-Node-ID': id } }).then(() => {
|
|
||||||
notification.success({
|
|
||||||
message: $gettext('Duplicate successfully'),
|
|
||||||
description:
|
|
||||||
$gettext('Duplicate %{conf_name} to %{node_name} successfully', { conf_name: props.name, node_name: node_map[id] }),
|
|
||||||
})
|
|
||||||
}).catch(e => {
|
|
||||||
notification.error({
|
|
||||||
message: $gettext('Duplicate failed'),
|
|
||||||
description: $gettext(e?.message ?? 'Server error'),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
if (r.enabled) {
|
|
||||||
domain.enable(modelRef.name, { headers: { 'X-Node-ID': id } }).then(() => {
|
|
||||||
notification.success({
|
|
||||||
message: $gettext('Enabled successfully'),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
loading.value = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.visible, v => {
|
|
||||||
if (v) {
|
|
||||||
modelRef.name = props.name // default with source name
|
|
||||||
modelRef.target = [0]
|
|
||||||
nextTick(() => clearValidate())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(() => gettext.current, () => {
|
|
||||||
clearValidate()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<AModal
|
|
||||||
v-model:open="show"
|
|
||||||
:title="$gettext('Duplicate')"
|
|
||||||
:confirm-loading="loading"
|
|
||||||
:mask="false"
|
|
||||||
@ok="onSubmit"
|
|
||||||
>
|
|
||||||
<AForm layout="vertical">
|
|
||||||
<AFormItem
|
|
||||||
:label="$gettext('Name')"
|
|
||||||
v-bind="validateInfos.name"
|
|
||||||
>
|
|
||||||
<AInput v-model:value="modelRef.name" />
|
|
||||||
</AFormItem>
|
|
||||||
<AFormItem
|
|
||||||
v-if="!settings.is_remote"
|
|
||||||
:label="$gettext('Target')"
|
|
||||||
v-bind="validateInfos.target"
|
|
||||||
>
|
|
||||||
<NodeSelector
|
|
||||||
v-model:target="modelRef.target"
|
|
||||||
v-model:map="node_map"
|
|
||||||
/>
|
|
||||||
</AFormItem>
|
|
||||||
</AForm>
|
|
||||||
</AModal>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
|
@ -1,9 +1,9 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Site } from '@/api/domain'
|
|
||||||
import type { ChatComplicationMessage } from '@/api/openai'
|
import type { ChatComplicationMessage } from '@/api/openai'
|
||||||
|
import type { Site } from '@/api/site'
|
||||||
import type { CheckedType } from '@/types'
|
import type { CheckedType } from '@/types'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import domain from '@/api/domain'
|
import site from '@/api/site'
|
||||||
import site_category from '@/api/site_category'
|
import site_category from '@/api/site_category'
|
||||||
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
|
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
|
||||||
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
|
import NodeSelector from '@/components/NodeSelector/NodeSelector.vue'
|
||||||
|
@ -11,6 +11,7 @@ import StdSelector from '@/components/StdDesign/StdDataEntry/components/StdSelec
|
||||||
import { formatDateTime } from '@/lib/helper'
|
import { formatDateTime } from '@/lib/helper'
|
||||||
import { useSettingsStore } from '@/pinia'
|
import { useSettingsStore } from '@/pinia'
|
||||||
import siteCategoryColumns from '@/views/site/site_category/columns'
|
import siteCategoryColumns from '@/views/site/site_category/columns'
|
||||||
|
import ConfigName from '@/views/site/site_edit/components/ConfigName.vue'
|
||||||
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
import { InfoCircleOutlined } from '@ant-design/icons-vue'
|
||||||
import { message, Modal } from 'ant-design-vue'
|
import { message, Modal } from 'ant-design-vue'
|
||||||
|
|
||||||
|
@ -18,18 +19,17 @@ const settings = useSettingsStore()
|
||||||
|
|
||||||
const configText = inject('configText') as Ref<string>
|
const configText = inject('configText') as Ref<string>
|
||||||
const enabled = inject('enabled') as Ref<boolean>
|
const enabled = inject('enabled') as Ref<boolean>
|
||||||
const name = inject('name') as Ref<string>
|
const name = inject('name') as ComputedRef<string>
|
||||||
const filepath = inject('filepath') as Ref<string>
|
const filepath = inject('filepath') as Ref<string>
|
||||||
const history_chatgpt_record = inject('history_chatgpt_record') as Ref<ChatComplicationMessage[]>
|
const historyChatgptRecord = inject('history_chatgpt_record') as Ref<ChatComplicationMessage[]>
|
||||||
const filename = inject('filename') as Ref<string | number | undefined>
|
|
||||||
const data = inject('data') as Ref<Site>
|
const data = inject('data') as Ref<Site>
|
||||||
|
|
||||||
const [modal, ContextHolder] = Modal.useModal()
|
const [modal, ContextHolder] = Modal.useModal()
|
||||||
|
|
||||||
const active_key = ref(['1', '2', '3'])
|
const activeKey = ref(['1', '2', '3'])
|
||||||
|
|
||||||
function enable() {
|
function enable() {
|
||||||
domain.enable(name.value).then(() => {
|
site.enable(name.value).then(() => {
|
||||||
message.success($gettext('Enabled successfully'))
|
message.success($gettext('Enabled successfully'))
|
||||||
enabled.value = true
|
enabled.value = true
|
||||||
}).catch(r => {
|
}).catch(r => {
|
||||||
|
@ -38,7 +38,7 @@ function enable() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function disable() {
|
function disable() {
|
||||||
domain.disable(name.value).then(() => {
|
site.disable(name.value).then(() => {
|
||||||
message.success($gettext('Disabled successfully'))
|
message.success($gettext('Disabled successfully'))
|
||||||
enabled.value = false
|
enabled.value = false
|
||||||
}).catch(r => {
|
}).catch(r => {
|
||||||
|
@ -46,7 +46,7 @@ function disable() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function on_change_enabled(checked: CheckedType) {
|
function onChangeEnabled(checked: CheckedType) {
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: checked ? $gettext('Do you want to enable this site?') : $gettext('Do you want to disable this site?'),
|
title: checked ? $gettext('Do you want to enable this site?') : $gettext('Do you want to disable this site?'),
|
||||||
mask: false,
|
mask: false,
|
||||||
|
@ -70,7 +70,7 @@ function on_change_enabled(checked: CheckedType) {
|
||||||
>
|
>
|
||||||
<ContextHolder />
|
<ContextHolder />
|
||||||
<ACollapse
|
<ACollapse
|
||||||
v-model:active-key="active_key"
|
v-model:active-key="activeKey"
|
||||||
ghost
|
ghost
|
||||||
collapsible="header"
|
collapsible="header"
|
||||||
>
|
>
|
||||||
|
@ -82,11 +82,11 @@ function on_change_enabled(checked: CheckedType) {
|
||||||
<AFormItem :label="$gettext('Enabled')">
|
<AFormItem :label="$gettext('Enabled')">
|
||||||
<ASwitch
|
<ASwitch
|
||||||
:checked="enabled"
|
:checked="enabled"
|
||||||
@change="on_change_enabled"
|
@change="onChangeEnabled"
|
||||||
/>
|
/>
|
||||||
</AFormItem>
|
</AFormItem>
|
||||||
<AFormItem :label="$gettext('Name')">
|
<AFormItem :label="$gettext('Name')">
|
||||||
<AInput v-model:value="filename" />
|
<ConfigName v-if="name" :name />
|
||||||
</AFormItem>
|
</AFormItem>
|
||||||
<AFormItem :label="$gettext('Category')">
|
<AFormItem :label="$gettext('Category')">
|
||||||
<StdSelector
|
<StdSelector
|
||||||
|
@ -138,7 +138,7 @@ function on_change_enabled(checked: CheckedType) {
|
||||||
header="ChatGPT"
|
header="ChatGPT"
|
||||||
>
|
>
|
||||||
<ChatGPT
|
<ChatGPT
|
||||||
v-model:history-messages="history_chatgpt_record"
|
v-model:history-messages="historyChatgptRecord"
|
||||||
:content="configText"
|
:content="configText"
|
||||||
:path="filepath"
|
:path="filepath"
|
||||||
/>
|
/>
|
|
@ -1,27 +1,23 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { CertificateInfo } from '@/api/cert'
|
import type { CertificateInfo } from '@/api/cert'
|
||||||
import type { Site } from '@/api/domain'
|
|
||||||
import type { NgxConfig } from '@/api/ngx'
|
import type { NgxConfig } from '@/api/ngx'
|
||||||
|
|
||||||
import type { ChatComplicationMessage } from '@/api/openai'
|
import type { ChatComplicationMessage } from '@/api/openai'
|
||||||
|
|
||||||
|
import type { Site } from '@/api/site'
|
||||||
import type { CheckedType } from '@/types'
|
import type { CheckedType } from '@/types'
|
||||||
import config from '@/api/config'
|
import config from '@/api/config'
|
||||||
import domain from '@/api/domain'
|
|
||||||
import ngx from '@/api/ngx'
|
import ngx from '@/api/ngx'
|
||||||
|
import site from '@/api/site'
|
||||||
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
|
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
|
||||||
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
|
||||||
import RightSettings from '@/views/site/components/RightSettings.vue'
|
|
||||||
import NgxConfigEditor from '@/views/site/ngx_conf/NgxConfigEditor.vue'
|
import NgxConfigEditor from '@/views/site/ngx_conf/NgxConfigEditor.vue'
|
||||||
|
import RightSettings from '@/views/site/site_edit/RightSettings.vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const name = ref(route.params.name.toString())
|
const name = computed(() => route.params?.name?.toString() ?? '')
|
||||||
|
|
||||||
watch(route, () => {
|
|
||||||
name.value = route.params?.name?.toString() ?? ''
|
|
||||||
})
|
|
||||||
|
|
||||||
const ngx_config: NgxConfig = reactive({
|
const ngx_config: NgxConfig = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
|
@ -77,7 +73,7 @@ function handle_response(r: Site) {
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
if (name.value) {
|
if (name.value) {
|
||||||
domain.get(name.value).then(r => {
|
site.get(name.value).then(r => {
|
||||||
handle_response(r)
|
handle_response(r)
|
||||||
}).catch(handle_parse_error)
|
}).catch(handle_parse_error)
|
||||||
}
|
}
|
||||||
|
@ -96,7 +92,7 @@ function handle_parse_error(e: { error?: string, message: string }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function on_mode_change(advanced: CheckedType) {
|
function on_mode_change(advanced: CheckedType) {
|
||||||
domain.advance_mode(name.value, { advanced: advanced as boolean }).then(() => {
|
site.advance_mode(name.value, { advanced: advanced as boolean }).then(() => {
|
||||||
advanceMode.value = advanced as boolean
|
advanceMode.value = advanced as boolean
|
||||||
if (advanced) {
|
if (advanced) {
|
||||||
build_config()
|
build_config()
|
||||||
|
@ -130,8 +126,7 @@ async function save() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return domain.save(name.value, {
|
return site.save(name.value, {
|
||||||
name: filename.value || name.value,
|
|
||||||
content: configText.value,
|
content: configText.value,
|
||||||
overwrite: true,
|
overwrite: true,
|
||||||
site_category_id: data.value.site_category_id,
|
site_category_id: data.value.site_category_id,
|
||||||
|
@ -154,7 +149,6 @@ provide('ngx_config', ngx_config)
|
||||||
provide('history_chatgpt_record', history_chatgpt_record)
|
provide('history_chatgpt_record', history_chatgpt_record)
|
||||||
provide('enabled', enabled)
|
provide('enabled', enabled)
|
||||||
provide('name', name)
|
provide('name', name)
|
||||||
provide('filename', filename)
|
|
||||||
provide('filepath', filepath)
|
provide('filepath', filepath)
|
||||||
provide('data', data)
|
provide('data', data)
|
||||||
</script>
|
</script>
|
63
app/src/views/site/site_edit/components/ConfigName.vue
Normal file
63
app/src/views/site/site_edit/components/ConfigName.vue
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import site from '@/api/site'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
name: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const modify = ref(false)
|
||||||
|
const buffer = ref('')
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
buffer.value = props.name
|
||||||
|
})
|
||||||
|
|
||||||
|
function clickModify() {
|
||||||
|
modify.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
loading.value = true
|
||||||
|
site.rename(props.name, buffer.value).then(() => {
|
||||||
|
modify.value = false
|
||||||
|
message.success($gettext('Renamed successfully'))
|
||||||
|
router.push({
|
||||||
|
path: `/sites/${buffer.value}`,
|
||||||
|
})
|
||||||
|
}).catch(e => {
|
||||||
|
message.error($gettext(e?.message ?? 'Server error'))
|
||||||
|
}).finally(() => {
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="!modify" class="flex items-center">
|
||||||
|
<div class="mr-2">
|
||||||
|
{{ buffer }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<AButton type="link" size="small" @click="clickModify">
|
||||||
|
{{ $gettext('Rename') }}
|
||||||
|
</AButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<AInput v-model:value="buffer">
|
||||||
|
<template #suffix>
|
||||||
|
<AButton :disabled="buffer === name" type="link" size="small" :loading @click="save">
|
||||||
|
{{ $gettext('Save') }}
|
||||||
|
</AButton>
|
||||||
|
</template>
|
||||||
|
</AInput>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
|
||||||
|
</style>
|
92
app/src/views/site/site_list/SiteDuplicate.vue
Normal file
92
app/src/views/site/site_list/SiteDuplicate.vue
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import site from '@/api/site'
|
||||||
|
|
||||||
|
import gettext from '@/gettext'
|
||||||
|
import { Form, message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible: boolean
|
||||||
|
name: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'duplicated'])
|
||||||
|
|
||||||
|
const show = computed({
|
||||||
|
get() {
|
||||||
|
return props.visible
|
||||||
|
},
|
||||||
|
set(v) {
|
||||||
|
emit('update:visible', v)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
interface Model {
|
||||||
|
name: string // site name
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelRef: Model = reactive({ name: '' })
|
||||||
|
|
||||||
|
const rulesRef = reactive({
|
||||||
|
name: [
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: () => $gettext('Please input name, '
|
||||||
|
+ 'this will be used as the filename of the new configuration.'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const { validate, validateInfos, clearValidate } = Form.useForm(modelRef, rulesRef)
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
validate().then(async () => {
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
site.duplicate(props.name, { name: modelRef.name }).then(() => {
|
||||||
|
message.success($gettext('Duplicate to local successfully'))
|
||||||
|
show.value = false
|
||||||
|
emit('duplicated')
|
||||||
|
}).catch(e => {
|
||||||
|
message.error($gettext(e?.message ?? 'Server error'))
|
||||||
|
})
|
||||||
|
|
||||||
|
loading.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => props.visible, v => {
|
||||||
|
if (v) {
|
||||||
|
modelRef.name = props.name // default with source name
|
||||||
|
nextTick(() => clearValidate())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => gettext.current, () => {
|
||||||
|
clearValidate()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<AModal
|
||||||
|
v-model:open="show"
|
||||||
|
:title="$gettext('Duplicate')"
|
||||||
|
:confirm-loading="loading"
|
||||||
|
:mask="false"
|
||||||
|
@ok="onSubmit"
|
||||||
|
>
|
||||||
|
<AForm layout="vertical">
|
||||||
|
<AFormItem
|
||||||
|
:label="$gettext('Name')"
|
||||||
|
v-bind="validateInfos.name"
|
||||||
|
>
|
||||||
|
<AInput v-model:value="modelRef.name" />
|
||||||
|
</AFormItem>
|
||||||
|
</AForm>
|
||||||
|
</AModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
|
||||||
|
</style>
|
|
@ -1,15 +1,15 @@
|
||||||
<script setup lang="tsx">
|
<script setup lang="tsx">
|
||||||
import type { Site } from '@/api/domain'
|
import type { Site } from '@/api/site'
|
||||||
import type { SiteCategory } from '@/api/site_category'
|
import type { SiteCategory } from '@/api/site_category'
|
||||||
import type { Column } from '@/components/StdDesign/types'
|
import type { Column } from '@/components/StdDesign/types'
|
||||||
import domain from '@/api/domain'
|
import site from '@/api/site'
|
||||||
import site_category from '@/api/site_category'
|
import site_category from '@/api/site_category'
|
||||||
|
import StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
|
||||||
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
|
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
|
||||||
import InspectConfig from '@/views/config/InspectConfig.vue'
|
import InspectConfig from '@/views/config/InspectConfig.vue'
|
||||||
import SiteDuplicate from '@/views/site/components/SiteDuplicate.vue'
|
|
||||||
import columns from '@/views/site/site_list/columns'
|
import columns from '@/views/site/site_list/columns'
|
||||||
|
import SiteDuplicate from '@/views/site/site_list/SiteDuplicate.vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import StdBatchEdit from '../../../components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
|
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -43,7 +43,7 @@ onMounted(async () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
function enable(name: string) {
|
function enable(name: string) {
|
||||||
domain.enable(name).then(() => {
|
site.enable(name).then(() => {
|
||||||
message.success($gettext('Enabled successfully'))
|
message.success($gettext('Enabled successfully'))
|
||||||
table.value?.get_list()
|
table.value?.get_list()
|
||||||
inspect_config.value?.test()
|
inspect_config.value?.test()
|
||||||
|
@ -53,7 +53,7 @@ function enable(name: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function disable(name: string) {
|
function disable(name: string) {
|
||||||
domain.disable(name).then(() => {
|
site.disable(name).then(() => {
|
||||||
message.success($gettext('Disabled successfully'))
|
message.success($gettext('Disabled successfully'))
|
||||||
table.value?.get_list()
|
table.value?.get_list()
|
||||||
inspect_config.value?.test()
|
inspect_config.value?.test()
|
||||||
|
@ -63,7 +63,7 @@ function disable(name: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function destroy(site_name: string) {
|
function destroy(site_name: string) {
|
||||||
domain.destroy(site_name).then(() => {
|
site.destroy(site_name).then(() => {
|
||||||
table.value.get_list()
|
table.value.get_list()
|
||||||
message.success($gettext('Delete site: %{site_name}', { site_name }))
|
message.success($gettext('Delete site: %{site_name}', { site_name }))
|
||||||
inspect_config.value?.test()
|
inspect_config.value?.test()
|
||||||
|
@ -104,7 +104,7 @@ function handleBatchUpdated() {
|
||||||
|
|
||||||
<StdTable
|
<StdTable
|
||||||
ref="table"
|
ref="table"
|
||||||
:api="domain"
|
:api="site"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
disable-delete
|
disable-delete
|
||||||
|
@ -162,7 +162,7 @@ function handleBatchUpdated() {
|
||||||
</StdTable>
|
</StdTable>
|
||||||
<StdBatchEdit
|
<StdBatchEdit
|
||||||
ref="stdBatchEditRef"
|
ref="stdBatchEditRef"
|
||||||
:api="domain"
|
:api="site"
|
||||||
:columns
|
:columns
|
||||||
@save="handleBatchUpdated"
|
@save="handleBatchUpdated"
|
||||||
/>
|
/>
|
||||||
|
|
10
go.mod
10
go.mod
|
@ -17,6 +17,7 @@ require (
|
||||||
github.com/go-acme/lego/v4 v4.19.2
|
github.com/go-acme/lego/v4 v4.19.2
|
||||||
github.com/go-co-op/gocron/v2 v2.12.1
|
github.com/go-co-op/gocron/v2 v2.12.1
|
||||||
github.com/go-playground/validator/v10 v10.22.1
|
github.com/go-playground/validator/v10 v10.22.1
|
||||||
|
github.com/go-resty/resty/v2 v2.15.3
|
||||||
github.com/go-webauthn/webauthn v0.11.2
|
github.com/go-webauthn/webauthn v0.11.2
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
|
@ -72,7 +73,7 @@ require (
|
||||||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||||
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
github.com/akamai/AkamaiOPEN-edgegrid-golang v1.2.2 // indirect
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.38 // indirect
|
github.com/aliyun/alibaba-cloud-sdk-go v1.63.39 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2 v1.32.2 // indirect
|
github.com/aws/aws-sdk-go-v2 v1.32.2 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.28.0 // indirect
|
github.com/aws/aws-sdk-go-v2/config v1.28.0 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.41 // indirect
|
||||||
|
@ -120,7 +121,6 @@ require (
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-resty/resty/v2 v2.15.3 // indirect
|
|
||||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
github.com/go-webauthn/x v0.1.15 // indirect
|
github.com/go-webauthn/x v0.1.15 // indirect
|
||||||
|
@ -143,7 +143,7 @@ require (
|
||||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.118 // indirect
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.119 // indirect
|
||||||
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df // indirect
|
||||||
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
github.com/infobloxopen/infoblox-go-client v1.1.1 // indirect
|
||||||
github.com/itchyny/timefmt-go v0.1.6 // indirect
|
github.com/itchyny/timefmt-go v0.1.6 // indirect
|
||||||
|
@ -215,7 +215,7 @@ require (
|
||||||
github.com/shopspring/decimal v1.4.0 // indirect
|
github.com/shopspring/decimal v1.4.0 // indirect
|
||||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||||
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9 // indirect
|
||||||
github.com/softlayer/softlayer-go v1.1.6 // indirect
|
github.com/softlayer/softlayer-go v1.1.7 // indirect
|
||||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e // indirect
|
||||||
github.com/sony/gobreaker v1.0.0 // indirect
|
github.com/sony/gobreaker v1.0.0 // indirect
|
||||||
github.com/sony/sonyflake v1.2.0 // indirect
|
github.com/sony/sonyflake v1.2.0 // indirect
|
||||||
|
@ -236,7 +236,7 @@ require (
|
||||||
github.com/uozi-tech/cosy-driver-mysql v0.2.2 // indirect
|
github.com/uozi-tech/cosy-driver-mysql v0.2.2 // indirect
|
||||||
github.com/uozi-tech/cosy-driver-postgres v0.2.1 // indirect
|
github.com/uozi-tech/cosy-driver-postgres v0.2.1 // indirect
|
||||||
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
github.com/vinyldns/go-vinyldns v0.9.16 // indirect
|
||||||
github.com/vultr/govultr/v3 v3.11.0 // indirect
|
github.com/vultr/govultr/v3 v3.11.1 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/yandex-cloud/go-genproto v0.0.0-20241021132621-28bb61d00c2f // indirect
|
github.com/yandex-cloud/go-genproto v0.0.0-20241021132621-28bb61d00c2f // indirect
|
||||||
github.com/yandex-cloud/go-sdk v0.0.0-20241021153520-213d4c625eca // indirect
|
github.com/yandex-cloud/go-sdk v0.0.0-20241021153520-213d4c625eca // indirect
|
||||||
|
|
16
go.sum
16
go.sum
|
@ -683,8 +683,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
|
||||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.38 h1:MVTkZJ63DE8XMVLQ5a0M1Elv+RHePK8UPrKjDdgbzDM=
|
github.com/aliyun/alibaba-cloud-sdk-go v1.63.39 h1:zlenrBGDiSEu7YnpWiAPscKNolgIo9Z6jvM5pcWAEL4=
|
||||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.38/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
|
github.com/aliyun/alibaba-cloud-sdk-go v1.63.39/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
|
||||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
|
github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0=
|
||||||
|
@ -1161,8 +1161,8 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
|
||||||
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4=
|
||||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.118 h1:YHcixaT7Le4PxuxN07KQ5j9nPeH4ZdyXtMTSgA+Whh8=
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.119 h1:2pi/hbcuv0CNVcsODkTYZY+X9j5uc1GTjSjX1cWMp/4=
|
||||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.118/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI=
|
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.119/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI=
|
||||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
@ -1580,8 +1580,8 @@ github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
github.com/smartystreets/gunit v1.1.3 h1:32x+htJCu3aMswhPw3teoJ+PnWPONqdNgaGs6Qt8ZaU=
|
github.com/smartystreets/gunit v1.1.3 h1:32x+htJCu3aMswhPw3teoJ+PnWPONqdNgaGs6Qt8ZaU=
|
||||||
github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
|
github.com/smartystreets/gunit v1.1.3/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ=
|
||||||
github.com/softlayer/softlayer-go v1.1.6 h1:VRNXiXZTpb7cfKjimU5E7W9zzKYzWMr/xtqlJ0pHwkQ=
|
github.com/softlayer/softlayer-go v1.1.7 h1:SgTL+pQZt1h+5QkAhVmHORM/7N9c1X0sljJhuOIHxWE=
|
||||||
github.com/softlayer/softlayer-go v1.1.6/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw=
|
github.com/softlayer/softlayer-go v1.1.7/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw=
|
||||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ=
|
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ=
|
||||||
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums=
|
github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums=
|
||||||
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
|
||||||
|
@ -1682,8 +1682,8 @@ github.com/uozi-tech/cosy-driver-sqlite v0.2.0 h1:eTpIMyGoFUK4JcaiKfJHD5AyiM6vtC
|
||||||
github.com/uozi-tech/cosy-driver-sqlite v0.2.0/go.mod h1:87a6mzn5IuEtIR4z7U4Ey8eKLGfNEOSkv7kPQlbNQgM=
|
github.com/uozi-tech/cosy-driver-sqlite v0.2.0/go.mod h1:87a6mzn5IuEtIR4z7U4Ey8eKLGfNEOSkv7kPQlbNQgM=
|
||||||
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
|
github.com/vinyldns/go-vinyldns v0.9.16 h1:GZJStDkcCk1F1AcRc64LuuMh+ENL8pHA0CVd4ulRMcQ=
|
||||||
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
|
github.com/vinyldns/go-vinyldns v0.9.16/go.mod h1:5qIJOdmzAnatKjurI+Tl4uTus7GJKJxb+zitufjHs3Q=
|
||||||
github.com/vultr/govultr/v3 v3.11.0 h1:YlAal70AaJ0k848RqcmjAzFcmLS9n8VtPgU68UxvVm8=
|
github.com/vultr/govultr/v3 v3.11.1 h1:Wc6wFTwh/gBZlOqSK1Hn3P9JWoFa7NCf52vGLwQcJOg=
|
||||||
github.com/vultr/govultr/v3 v3.11.0/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
|
github.com/vultr/govultr/v3 v3.11.1/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
|
||||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||||
|
|
|
@ -2,8 +2,17 @@ package helper
|
||||||
|
|
||||||
import "os"
|
import "os"
|
||||||
|
|
||||||
func FileExists(filename string) bool {
|
func FileExists(filepath string) bool {
|
||||||
_, err := os.Stat(filename)
|
_, err := os.Stat(filepath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func SymbolLinkExists(filepath string) bool {
|
||||||
|
_, err := os.Lstat(filepath)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
85
internal/site/delete.go
Normal file
85
internal/site/delete.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package site
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||||
|
"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/go-resty/resty/v2"
|
||||||
|
"github.com/uozi-tech/cosy/logger"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete deletes a site by removing the file in sites-available
|
||||||
|
func Delete(name string) (err error) {
|
||||||
|
availablePath := nginx.GetConfPath("sites-available", name)
|
||||||
|
|
||||||
|
s := query.Site
|
||||||
|
_, err = s.Where(s.Path.Eq(availablePath)).Unscoped().Delete(&model.Site{})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledPath := nginx.GetConfPath("sites-enabled", name)
|
||||||
|
|
||||||
|
if !helper.FileExists(availablePath) {
|
||||||
|
return fmt.Errorf("site not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if helper.FileExists(enabledPath) {
|
||||||
|
return fmt.Errorf("site is enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
certModel := model.Cert{Filename: name}
|
||||||
|
_ = certModel.Remove()
|
||||||
|
|
||||||
|
err = os.Remove(availablePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go syncDelete(name)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDelete(name string) {
|
||||||
|
nodes := getSyncNodes(name)
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(len(nodes))
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
runtime.Stack(buf, false)
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
client := resty.New()
|
||||||
|
client.SetBaseURL(node.URL)
|
||||||
|
resp, err := client.R().
|
||||||
|
Delete(fmt.Sprintf("/api/sites/%s", name))
|
||||||
|
if err != nil {
|
||||||
|
notification.Error("Delete Remote Site Error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.StatusCode() != http.StatusOK {
|
||||||
|
notification.Error("Delete Remote Site Error", string(resp.Body()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notification.Success("Delete Remote Site Success", string(resp.Body()))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
80
internal/site/disable.go
Normal file
80
internal/site/disable.go
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
package site
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/notification"
|
||||||
|
"github.com/0xJacky/Nginx-UI/model"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/uozi-tech/cosy/logger"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Disable disables a site by removing the symlink in sites-enabled
|
||||||
|
func Disable(name string) (err error) {
|
||||||
|
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
|
||||||
|
_, err = os.Stat(enabledConfigFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Remove(enabledConfigFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete auto cert record
|
||||||
|
certModel := model.Cert{Filename: name}
|
||||||
|
err = certModel.Remove()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
output := nginx.Reload()
|
||||||
|
if nginx.GetLogLevel(output) > nginx.Warn {
|
||||||
|
return fmt.Errorf(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
go syncDisable(name)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncDisable(name string) {
|
||||||
|
nodes := getSyncNodes(name)
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(len(nodes))
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
runtime.Stack(buf, false)
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
client := resty.New()
|
||||||
|
client.SetBaseURL(node.URL)
|
||||||
|
resp, err := client.R().
|
||||||
|
Post(fmt.Sprintf("/api/sites/%s/disable", name))
|
||||||
|
if err != nil {
|
||||||
|
notification.Error("Disable Remote Site Error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.StatusCode() != http.StatusOK {
|
||||||
|
notification.Error("Disable Remote Site Error", string(resp.Body()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notification.Success("Disable Remote Site Success", string(resp.Body()))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
24
internal/site/duplicate.go
Normal file
24
internal/site/duplicate.go
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
package site
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Duplicate duplicates a site by copying the file
|
||||||
|
func Duplicate(src, dst string) (err error) {
|
||||||
|
src = nginx.GetConfPath("sites-available", src)
|
||||||
|
dst = nginx.GetConfPath("sites-available", dst)
|
||||||
|
|
||||||
|
if helper.FileExists(dst) {
|
||||||
|
return errors.New("file exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = helper.CopyFile(src, dst)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
86
internal/site/enable.go
Normal file
86
internal/site/enable.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
package site
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/notification"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/uozi-tech/cosy/logger"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Enable enables a site by creating a symlink in sites-enabled
|
||||||
|
func Enable(name string) (err error) {
|
||||||
|
configFilePath := nginx.GetConfPath("sites-available", name)
|
||||||
|
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
|
||||||
|
|
||||||
|
_, err = os.Stat(configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if helper.FileExists(enabledConfigFilePath) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Symlink(configFilePath, enabledConfigFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test nginx config, if not pass, then disable the site.
|
||||||
|
output := nginx.TestConf()
|
||||||
|
if nginx.GetLogLevel(output) > nginx.Warn {
|
||||||
|
_ = os.Remove(enabledConfigFilePath)
|
||||||
|
return fmt.Errorf(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
output = nginx.Reload()
|
||||||
|
if nginx.GetLogLevel(output) > nginx.Warn {
|
||||||
|
return fmt.Errorf(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
go syncEnable(name)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncEnable(name string) {
|
||||||
|
nodes := getSyncNodes(name)
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(len(nodes))
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
runtime.Stack(buf, false)
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
client := resty.New()
|
||||||
|
client.SetBaseURL(node.URL)
|
||||||
|
resp, err := client.R().
|
||||||
|
Post(fmt.Sprintf("/api/sites/%s/enable", name))
|
||||||
|
if err != nil {
|
||||||
|
notification.Error("Enable Remote Site Error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.StatusCode() != http.StatusOK {
|
||||||
|
notification.Error("Enable Remote Site Error", string(resp.Body()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notification.Success("Enable Remote Site Success", string(resp.Body()))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
103
internal/site/rename.go
Normal file
103
internal/site/rename.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package site
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/notification"
|
||||||
|
"github.com/0xJacky/Nginx-UI/query"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
"github.com/uozi-tech/cosy/logger"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Rename(oldName string, newName string) (err error) {
|
||||||
|
oldPath := nginx.GetConfPath("sites-available", oldName)
|
||||||
|
newPath := nginx.GetConfPath("sites-available", newName)
|
||||||
|
|
||||||
|
if oldPath == newPath {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if dst file exists, do not rename
|
||||||
|
if helper.FileExists(newPath) {
|
||||||
|
return fmt.Errorf("file exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
s := query.Site
|
||||||
|
_, _ = s.Where(s.Path.Eq(oldPath)).Update(s.Path, newPath)
|
||||||
|
|
||||||
|
err = os.Rename(oldPath, newPath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// recreate a soft link
|
||||||
|
oldEnabledConfigFilePath := nginx.GetConfPath("sites-enabled", oldName)
|
||||||
|
if helper.SymbolLinkExists(oldEnabledConfigFilePath) {
|
||||||
|
_ = os.Remove(oldEnabledConfigFilePath)
|
||||||
|
newEnabledConfigFilePath := nginx.GetConfPath("sites-enabled", newName)
|
||||||
|
err = os.Symlink(newPath, newEnabledConfigFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test nginx configuration
|
||||||
|
output := nginx.TestConf()
|
||||||
|
if nginx.GetLogLevel(output) > nginx.Warn {
|
||||||
|
return fmt.Errorf(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload nginx
|
||||||
|
output = nginx.Reload()
|
||||||
|
if nginx.GetLogLevel(output) > nginx.Warn {
|
||||||
|
return fmt.Errorf(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
go syncRename(oldName, newName)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncRename(oldName, newName string) {
|
||||||
|
nodes := getSyncNodes(newName)
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(len(nodes))
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
runtime.Stack(buf, false)
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
client := resty.New()
|
||||||
|
client.SetBaseURL(node.URL)
|
||||||
|
resp, err := client.R().
|
||||||
|
SetBody(map[string]string{
|
||||||
|
"new_name": newName,
|
||||||
|
}).
|
||||||
|
Post(fmt.Sprintf("/api/sites/%s/rename", oldName))
|
||||||
|
if err != nil {
|
||||||
|
notification.Error("Rename Remote Site Error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.StatusCode() != http.StatusOK {
|
||||||
|
notification.Error("Rename Remote Site Error", string(resp.Body()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notification.Success("Rename Remote Site Success", string(resp.Body()))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
100
internal/site/save.go
Normal file
100
internal/site/save.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package site
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||||
|
"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/go-resty/resty/v2"
|
||||||
|
"github.com/uozi-tech/cosy/logger"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Save saves a site configuration file
|
||||||
|
func Save(name string, content string, overwrite bool, siteCategoryId uint64, syncNodeIds []uint64) (err error) {
|
||||||
|
path := nginx.GetConfPath("sites-available", name)
|
||||||
|
if !overwrite && helper.FileExists(path) {
|
||||||
|
return fmt.Errorf("file exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(path, []byte(content), 0644)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
|
||||||
|
if helper.FileExists(enabledConfigFilePath) {
|
||||||
|
// Test nginx configuration
|
||||||
|
output := nginx.TestConf()
|
||||||
|
|
||||||
|
if nginx.GetLogLevel(output) > nginx.Warn {
|
||||||
|
return fmt.Errorf(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
output = nginx.Reload()
|
||||||
|
|
||||||
|
if nginx.GetLogLevel(output) > nginx.Warn {
|
||||||
|
return fmt.Errorf(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s := query.Site
|
||||||
|
_, err = s.Where(s.Path.Eq(path)).
|
||||||
|
Select(s.SiteCategoryID, s.SyncNodeIDs).
|
||||||
|
Updates(&model.Site{
|
||||||
|
SiteCategoryID: siteCategoryId,
|
||||||
|
SyncNodeIDs: syncNodeIds,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go syncSave(name, content)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func syncSave(name string, content string) {
|
||||||
|
nodes := getSyncNodes(name)
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
wg.Add(len(nodes))
|
||||||
|
|
||||||
|
for _, node := range nodes {
|
||||||
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if err := recover(); err != nil {
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
runtime.Stack(buf, false)
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
client := resty.New()
|
||||||
|
client.SetBaseURL(node.URL)
|
||||||
|
resp, err := client.R().
|
||||||
|
SetBody(map[string]interface{}{
|
||||||
|
"content": content,
|
||||||
|
"overwrite": true,
|
||||||
|
}).
|
||||||
|
Post(fmt.Sprintf("/api/sites/%s", name))
|
||||||
|
if err != nil {
|
||||||
|
notification.Error("Save Remote Site Error", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resp.StatusCode() != http.StatusOK {
|
||||||
|
notification.Error("Save Remote Site Error", string(resp.Body()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notification.Success("Save Remote Site Success", string(resp.Body()))
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
35
internal/site/sync.go
Normal file
35
internal/site/sync.go
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
package site
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||||
|
"github.com/0xJacky/Nginx-UI/model"
|
||||||
|
"github.com/0xJacky/Nginx-UI/query"
|
||||||
|
"github.com/samber/lo"
|
||||||
|
"github.com/uozi-tech/cosy/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getSyncNodes(name string) (nodes []*model.Environment) {
|
||||||
|
configFilePath := nginx.GetConfPath("sites-available", name)
|
||||||
|
s := query.Site
|
||||||
|
site, err := s.Where(s.Path.Eq(configFilePath)).
|
||||||
|
Preload(s.SiteCategory).First()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
syncNodeIds := site.SyncNodeIDs
|
||||||
|
// inherit sync node ids from site category
|
||||||
|
if site.SiteCategory != nil {
|
||||||
|
syncNodeIds = append(syncNodeIds, site.SiteCategory.SyncNodeIds...)
|
||||||
|
}
|
||||||
|
syncNodeIds = lo.Uniq(syncNodeIds)
|
||||||
|
|
||||||
|
e := query.Environment
|
||||||
|
nodes, err = e.Where(e.ID.In(syncNodeIds...)).Find()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -50,9 +50,9 @@ type Cert struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func FirstCert(confName string) (c Cert, err error) {
|
func FirstCert(confName string) (c Cert, err error) {
|
||||||
err = db.First(&c, &Cert{
|
err = db.Limit(1).Where(&Cert{
|
||||||
Filename: confName,
|
Filename: confName,
|
||||||
}).Error
|
}).Find(&c).Error
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,6 @@ package model
|
||||||
|
|
||||||
type SiteCategory struct {
|
type SiteCategory struct {
|
||||||
Model
|
Model
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
SyncNodeIds []int `json:"sync_node_ids" gorm:"serializer:json"`
|
SyncNodeIds []uint64 `json:"sync_node_ids" gorm:"serializer:json"`
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue