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 (
|
||||
"github.com/0xJacky/Nginx-UI/api"
|
||||
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||
"github.com/0xJacky/Nginx-UI/internal/site"
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func DuplicateSite(c *gin.Context) {
|
||||
// Source name
|
||||
name := c.Param("name")
|
||||
src := c.Param("name")
|
||||
|
||||
// Destination name
|
||||
var json struct {
|
||||
|
@ -21,24 +20,13 @@ func DuplicateSite(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
src := nginx.GetConfPath("sites-available", 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)
|
||||
|
||||
err := site.Duplicate(src, json.Name)
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"dst": dst,
|
||||
"message": "ok",
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,17 +3,25 @@ package sites
|
|||
import "github.com/gin-gonic/gin"
|
||||
|
||||
func InitRouter(r *gin.RouterGroup) {
|
||||
r.GET("domains", GetSiteList)
|
||||
r.GET("domains/:name", GetSite)
|
||||
r.POST("domains/:name", SaveSite)
|
||||
r.PUT("domains", BatchUpdateSites)
|
||||
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.GET("sites", GetSiteList)
|
||||
r.GET("sites/:name", GetSite)
|
||||
r.PUT("sites", BatchUpdateSites)
|
||||
r.POST("sites/:name/advance", DomainEditByAdvancedMode)
|
||||
r.POST("auto_cert/:name", AddDomainToAutoCert)
|
||||
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) {
|
||||
|
|
|
@ -3,8 +3,8 @@ package sites
|
|||
import (
|
||||
"github.com/0xJacky/Nginx-UI/api"
|
||||
"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/site"
|
||||
"github.com/0xJacky/Nginx-UI/model"
|
||||
"github.com/0xJacky/Nginx-UI/query"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
@ -17,14 +17,8 @@ import (
|
|||
)
|
||||
|
||||
func GetSite(c *gin.Context) {
|
||||
rewriteName, ok := c.Get("rewriteConfigFileName")
|
||||
name := c.Param("name")
|
||||
|
||||
// for modify filename
|
||||
if ok {
|
||||
name = rewriteName.(string)
|
||||
}
|
||||
|
||||
path := nginx.GetConfPath("sites-available", name)
|
||||
file, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
|
@ -51,7 +45,7 @@ func GetSite(c *gin.Context) {
|
|||
}
|
||||
|
||||
s := query.Site
|
||||
site, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
|
||||
siteModel, err := s.Where(s.Path.Eq(path)).FirstOrCreate()
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
return
|
||||
|
@ -62,7 +56,7 @@ func GetSite(c *gin.Context) {
|
|||
logger.Warn(err)
|
||||
}
|
||||
|
||||
if site.Advanced {
|
||||
if siteModel.Advanced {
|
||||
origContent, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
|
@ -71,7 +65,7 @@ func GetSite(c *gin.Context) {
|
|||
|
||||
c.JSON(http.StatusOK, Site{
|
||||
ModifiedAt: file.ModTime(),
|
||||
Site: site,
|
||||
Site: siteModel,
|
||||
Enabled: enabled,
|
||||
Name: name,
|
||||
Config: string(origContent),
|
||||
|
@ -103,8 +97,8 @@ func GetSite(c *gin.Context) {
|
|||
}
|
||||
|
||||
c.JSON(http.StatusOK, Site{
|
||||
Site: siteModel,
|
||||
ModifiedAt: file.ModTime(),
|
||||
Site: site,
|
||||
Enabled: enabled,
|
||||
Name: name,
|
||||
Config: nginxConfig.FmtCode(),
|
||||
|
@ -119,15 +113,7 @@ func GetSite(c *gin.Context) {
|
|||
func SaveSite(c *gin.Context) {
|
||||
name := c.Param("name")
|
||||
|
||||
if name == "" {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{
|
||||
"message": "param name is empty",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var json struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Content string `json:"content" binding:"required"`
|
||||
SiteCategoryID uint64 `json:"site_category_id"`
|
||||
SyncNodeIDs []uint64 `json:"sync_node_ids"`
|
||||
|
@ -138,129 +124,39 @@ func SaveSite(c *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
path := nginx.GetConfPath("sites-available", name)
|
||||
|
||||
if !json.Overwrite && helper.FileExists(path) {
|
||||
c.JSON(http.StatusNotAcceptable, gin.H{
|
||||
"message": "File exists",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := os.WriteFile(path, []byte(json.Content), 0644)
|
||||
err := site.Save(name, json.Content, json.Overwrite, json.SiteCategoryID, json.SyncNodeIDs)
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
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)
|
||||
}
|
||||
|
||||
func EnableSite(c *gin.Context) {
|
||||
configFilePath := nginx.GetConfPath("sites-available", c.Param("name"))
|
||||
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
|
||||
|
||||
_, err := os.Stat(configFilePath)
|
||||
func RenameSite(c *gin.Context) {
|
||||
oldName := c.Param("name")
|
||||
var json struct {
|
||||
NewName string `json:"new_name"`
|
||||
}
|
||||
if !cosy.BindAndValid(c, &json) {
|
||||
return
|
||||
}
|
||||
|
||||
err := site.Rename(oldName, json.NewName)
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
|
||||
err = os.Symlink(configFilePath, enabledConfigFilePath)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "ok",
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
func EnableSite(c *gin.Context) {
|
||||
err := site.Enable(c.Param("name"))
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -270,71 +166,19 @@ func EnableSite(c *gin.Context) {
|
|||
}
|
||||
|
||||
func DisableSite(c *gin.Context) {
|
||||
enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
|
||||
_, err := os.Stat(enabledConfigFilePath)
|
||||
err := site.Disable(c.Param("name"))
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
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{
|
||||
"message": "ok",
|
||||
})
|
||||
}
|
||||
|
||||
func DeleteSite(c *gin.Context) {
|
||||
var err error
|
||||
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)
|
||||
err := site.Delete(c.Param("name"))
|
||||
if err != nil {
|
||||
api.ErrHandler(c, err)
|
||||
return
|
||||
|
|
|
@ -29,7 +29,7 @@ export interface AutoCertRequest {
|
|||
key_type: PrivateKeyType
|
||||
}
|
||||
|
||||
class Domain extends Curd<Site> {
|
||||
class SiteCurd extends Curd<Site> {
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
enable(name: string, config?: any) {
|
||||
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`)
|
||||
}
|
||||
|
||||
rename(oldName: string, newName: string) {
|
||||
return http.post(`${this.baseUrl}/${oldName}/rename`, { new_name: newName })
|
||||
}
|
||||
|
||||
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">
|
||||
import type { Column, JSXElements, StdDesignEdit } from '@/components/StdDesign/types'
|
||||
import type { FormInstance } from 'ant-design-vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { labelRender } from '@/components/StdDesign/StdDataEntry'
|
||||
import StdFormItem from '@/components/StdDesign/StdDataEntry/StdFormItem.vue'
|
||||
|
@ -7,26 +8,13 @@ import { Form } from 'ant-design-vue'
|
|||
|
||||
const props = defineProps<{
|
||||
dataList: Column[]
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
dataSource: Record<string, any>
|
||||
errors?: Record<string, string>
|
||||
type?: 'search' | 'edit'
|
||||
layout?: 'horizontal' | 'vertical' | 'inline'
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
'update:dataSource': [data: Record<string, any>]
|
||||
}>()
|
||||
|
||||
const dataSource = computed({
|
||||
get() {
|
||||
return props.dataSource
|
||||
},
|
||||
set(v) {
|
||||
emit('update:dataSource', v)
|
||||
},
|
||||
})
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
const dataSource = defineModel<Record<string, any>>('dataSource')
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
|
@ -37,7 +25,7 @@ function extraRender(extra?: string | (() => string)) {
|
|||
return extra
|
||||
}
|
||||
|
||||
const formRef = ref<InstanceType<typeof Form>>()
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
defineExpose({
|
||||
formRef,
|
||||
|
@ -50,7 +38,7 @@ function Render() {
|
|||
props.dataList.forEach((v: Column) => {
|
||||
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 (v.search) {
|
||||
const type = (v.search as StdDesignEdit)?.type || v.edit?.type
|
||||
|
@ -75,7 +63,7 @@ function Render() {
|
|||
|
||||
let show = true
|
||||
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) {
|
||||
template.push(
|
||||
|
@ -87,6 +75,7 @@ function Render() {
|
|||
error={props.errors}
|
||||
required={v.edit?.config?.required}
|
||||
hint={v.edit?.hint}
|
||||
noValidate={v.edit?.config?.noValidate}
|
||||
>
|
||||
{v.edit.type(v.edit, dataSource.value, dataIndex)}
|
||||
</StdFormItem>,
|
||||
|
@ -97,7 +86,16 @@ function Render() {
|
|||
if (slots.action)
|
||||
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>
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
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>()
|
||||
|
||||
|
@ -13,18 +14,42 @@ export interface Props {
|
|||
[key: string]: string
|
||||
}
|
||||
required?: boolean
|
||||
noValidate?: boolean
|
||||
}
|
||||
|
||||
const tag = computed(() => {
|
||||
return props.error?.[props.dataIndex!.toString()] ?? ''
|
||||
})
|
||||
|
||||
// const valid_status = computed(() => {
|
||||
// if (tag.value)
|
||||
// return 'error'
|
||||
// else return 'success'
|
||||
// })
|
||||
|
||||
const help = computed(() => {
|
||||
if (tag.value.includes('required'))
|
||||
return $gettext('This field should not be empty')
|
||||
const rules = tag.value.split(',')
|
||||
|
||||
for (const rule of rules) {
|
||||
if (FormErrors[rule])
|
||||
return FormErrors[rule]()
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -32,7 +57,9 @@ const help = computed(() => {
|
|||
:name="dataIndex as string"
|
||||
:label="label"
|
||||
:help="help"
|
||||
:required="required"
|
||||
:rules="{ required, validator }"
|
||||
:validate-status="tag ? 'error' : undefined"
|
||||
:auto-link="false"
|
||||
>
|
||||
<slot />
|
||||
</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?: {
|
||||
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
|
||||
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
|
||||
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
|
||||
addonAfter?: string // for inputNumber
|
||||
prefix?: string // for inputNumber
|
||||
suffix?: string // for inputNumber
|
||||
size?: string // class size of Std image upload
|
||||
error_messages?: Ref
|
||||
}
|
||||
|
||||
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 use2FAModal from '@/components/TwoFA/use2FAModal'
|
||||
import { useNProgress } from '@/lib/nprogress/nprogress'
|
||||
import { useSettingsStore, useUserStore } from '@/pinia'
|
||||
import router from '@/routes'
|
||||
import axios from 'axios'
|
||||
import NProgress from 'nprogress'
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
import 'nprogress/nprogress.css'
|
||||
|
@ -26,9 +26,11 @@ const instance = axios.create({
|
|||
}],
|
||||
})
|
||||
|
||||
const nprogress = useNProgress()
|
||||
|
||||
instance.interceptors.request.use(
|
||||
config => {
|
||||
NProgress.start()
|
||||
nprogress.start()
|
||||
if (token.value) {
|
||||
// eslint-disable-next-line ts/no-explicit-any
|
||||
(config.headers as any).Authorization = token.value
|
||||
|
@ -53,12 +55,12 @@ instance.interceptors.request.use(
|
|||
|
||||
instance.interceptors.response.use(
|
||||
response => {
|
||||
NProgress.done()
|
||||
nprogress.done()
|
||||
|
||||
return Promise.resolve(response.data)
|
||||
},
|
||||
async error => {
|
||||
NProgress.done()
|
||||
nprogress.done()
|
||||
|
||||
const otpModal = use2FAModal()
|
||||
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 { useSettingsStore, useUserStore } from '@/pinia'
|
||||
import { useNProgress } from '@/lib/nprogress/nprogress'
|
||||
|
||||
import { useSettingsStore, useUserStore } from '@/pinia'
|
||||
import {
|
||||
BellOutlined,
|
||||
CloudOutlined,
|
||||
|
@ -15,10 +16,8 @@ import {
|
|||
ShareAltOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons-vue'
|
||||
import NProgress from 'nprogress'
|
||||
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
|
||||
import 'nprogress/nprogress.css'
|
||||
|
||||
export const routes: RouteRecordRaw[] = [
|
||||
|
@ -74,7 +73,7 @@ export const routes: RouteRecordRaw[] = [
|
|||
}, {
|
||||
path: ':name',
|
||||
name: 'Edit Site',
|
||||
component: () => import('@/views/site/SiteEdit.vue'),
|
||||
component: () => import('@/views/site/site_edit/SiteEdit.vue'),
|
||||
meta: {
|
||||
name: () => $gettext('Edit Site'),
|
||||
hiddenInSidebar: true,
|
||||
|
@ -324,12 +323,12 @@ const router = createRouter({
|
|||
routes,
|
||||
})
|
||||
|
||||
NProgress.configure({ showSpinner: false })
|
||||
const nprogress = useNProgress()
|
||||
|
||||
router.beforeEach((to, _, next) => {
|
||||
document.title = `${to?.meta.name?.() ?? ''} | Nginx UI`
|
||||
|
||||
NProgress.start()
|
||||
nprogress.start()
|
||||
|
||||
const user = useUserStore()
|
||||
|
||||
|
@ -340,7 +339,7 @@ router.beforeEach((to, _, next) => {
|
|||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
NProgress.done()
|
||||
nprogress.done()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { NgxConfig } from '@/api/ngx'
|
||||
import domain from '@/api/domain'
|
||||
import ngx from '@/api/ngx'
|
||||
import site from '@/api/site'
|
||||
import DirectiveEditor from '@/views/site/ngx_conf/directive/DirectiveEditor.vue'
|
||||
import LocationEditor from '@/views/site/ngx_conf/LocationEditor.vue'
|
||||
import NgxConfigEditor from '@/views/site/ngx_conf/NgxConfigEditor.vue'
|
||||
|
@ -26,17 +26,17 @@ onMounted(() => {
|
|||
})
|
||||
|
||||
function init() {
|
||||
domain.get_template().then(r => {
|
||||
site.get_template().then(r => {
|
||||
Object.assign(ngx_config, r.tokenized)
|
||||
})
|
||||
}
|
||||
|
||||
async function save() {
|
||||
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'))
|
||||
|
||||
domain.enable(ngx_config.name).then(() => {
|
||||
site.enable(ngx_config.name).then(() => {
|
||||
message.success($gettext('Enabled successfully'))
|
||||
window.scroll({ top: 0, left: 0, behavior: 'smooth' })
|
||||
}).catch(e => {
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { CertificateResult } from '@/api/cert'
|
|||
import type { NgxConfig, NgxDirective } from '@/api/ngx'
|
||||
import type { PrivateKeyType } from '@/constants'
|
||||
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 ObtainCertLive from '@/views/site/cert/components/ObtainCertLive.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) {
|
||||
if (status) {
|
||||
domain.add_auto_cert(props.configName, {
|
||||
site.add_auto_cert(props.configName, {
|
||||
domains: name.value.trim().split(' '),
|
||||
challenge_method: data.value.challenge_method!,
|
||||
dns_credential_id: data.value.dns_credential_id!,
|
||||
|
@ -71,7 +71,7 @@ function change_auto_cert(status: boolean, key_type?: PrivateKeyType) {
|
|||
})
|
||||
}
|
||||
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 }))
|
||||
}).catch(e => {
|
||||
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">
|
||||
import type { Site } from '@/api/domain'
|
||||
import type { ChatComplicationMessage } from '@/api/openai'
|
||||
import type { Site } from '@/api/site'
|
||||
import type { CheckedType } from '@/types'
|
||||
import type { Ref } from 'vue'
|
||||
import domain from '@/api/domain'
|
||||
import site from '@/api/site'
|
||||
import site_category from '@/api/site_category'
|
||||
import ChatGPT from '@/components/ChatGPT/ChatGPT.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 { useSettingsStore } from '@/pinia'
|
||||
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 { message, Modal } from 'ant-design-vue'
|
||||
|
||||
|
@ -18,18 +19,17 @@ const settings = useSettingsStore()
|
|||
|
||||
const configText = inject('configText') as Ref<string>
|
||||
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 history_chatgpt_record = inject('history_chatgpt_record') as Ref<ChatComplicationMessage[]>
|
||||
const filename = inject('filename') as Ref<string | number | undefined>
|
||||
const historyChatgptRecord = inject('history_chatgpt_record') as Ref<ChatComplicationMessage[]>
|
||||
const data = inject('data') as Ref<Site>
|
||||
|
||||
const [modal, ContextHolder] = Modal.useModal()
|
||||
|
||||
const active_key = ref(['1', '2', '3'])
|
||||
const activeKey = ref(['1', '2', '3'])
|
||||
|
||||
function enable() {
|
||||
domain.enable(name.value).then(() => {
|
||||
site.enable(name.value).then(() => {
|
||||
message.success($gettext('Enabled successfully'))
|
||||
enabled.value = true
|
||||
}).catch(r => {
|
||||
|
@ -38,7 +38,7 @@ function enable() {
|
|||
}
|
||||
|
||||
function disable() {
|
||||
domain.disable(name.value).then(() => {
|
||||
site.disable(name.value).then(() => {
|
||||
message.success($gettext('Disabled successfully'))
|
||||
enabled.value = false
|
||||
}).catch(r => {
|
||||
|
@ -46,7 +46,7 @@ function disable() {
|
|||
})
|
||||
}
|
||||
|
||||
function on_change_enabled(checked: CheckedType) {
|
||||
function onChangeEnabled(checked: CheckedType) {
|
||||
modal.confirm({
|
||||
title: checked ? $gettext('Do you want to enable this site?') : $gettext('Do you want to disable this site?'),
|
||||
mask: false,
|
||||
|
@ -70,7 +70,7 @@ function on_change_enabled(checked: CheckedType) {
|
|||
>
|
||||
<ContextHolder />
|
||||
<ACollapse
|
||||
v-model:active-key="active_key"
|
||||
v-model:active-key="activeKey"
|
||||
ghost
|
||||
collapsible="header"
|
||||
>
|
||||
|
@ -82,11 +82,11 @@ function on_change_enabled(checked: CheckedType) {
|
|||
<AFormItem :label="$gettext('Enabled')">
|
||||
<ASwitch
|
||||
:checked="enabled"
|
||||
@change="on_change_enabled"
|
||||
@change="onChangeEnabled"
|
||||
/>
|
||||
</AFormItem>
|
||||
<AFormItem :label="$gettext('Name')">
|
||||
<AInput v-model:value="filename" />
|
||||
<ConfigName v-if="name" :name />
|
||||
</AFormItem>
|
||||
<AFormItem :label="$gettext('Category')">
|
||||
<StdSelector
|
||||
|
@ -138,7 +138,7 @@ function on_change_enabled(checked: CheckedType) {
|
|||
header="ChatGPT"
|
||||
>
|
||||
<ChatGPT
|
||||
v-model:history-messages="history_chatgpt_record"
|
||||
v-model:history-messages="historyChatgptRecord"
|
||||
:content="configText"
|
||||
:path="filepath"
|
||||
/>
|
|
@ -1,27 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import type { CertificateInfo } from '@/api/cert'
|
||||
import type { Site } from '@/api/domain'
|
||||
import type { NgxConfig } from '@/api/ngx'
|
||||
|
||||
import type { ChatComplicationMessage } from '@/api/openai'
|
||||
|
||||
import type { Site } from '@/api/site'
|
||||
import type { CheckedType } from '@/types'
|
||||
import config from '@/api/config'
|
||||
import domain from '@/api/domain'
|
||||
import ngx from '@/api/ngx'
|
||||
import site from '@/api/site'
|
||||
import CodeEditor from '@/components/CodeEditor/CodeEditor.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 RightSettings from '@/views/site/site_edit/RightSettings.vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const name = ref(route.params.name.toString())
|
||||
|
||||
watch(route, () => {
|
||||
name.value = route.params?.name?.toString() ?? ''
|
||||
})
|
||||
const name = computed(() => route.params?.name?.toString() ?? '')
|
||||
|
||||
const ngx_config: NgxConfig = reactive({
|
||||
name: '',
|
||||
|
@ -77,7 +73,7 @@ function handle_response(r: Site) {
|
|||
|
||||
function init() {
|
||||
if (name.value) {
|
||||
domain.get(name.value).then(r => {
|
||||
site.get(name.value).then(r => {
|
||||
handle_response(r)
|
||||
}).catch(handle_parse_error)
|
||||
}
|
||||
|
@ -96,7 +92,7 @@ function handle_parse_error(e: { error?: string, message: string }) {
|
|||
}
|
||||
|
||||
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
|
||||
if (advanced) {
|
||||
build_config()
|
||||
|
@ -130,8 +126,7 @@ async function save() {
|
|||
}
|
||||
}
|
||||
|
||||
return domain.save(name.value, {
|
||||
name: filename.value || name.value,
|
||||
return site.save(name.value, {
|
||||
content: configText.value,
|
||||
overwrite: true,
|
||||
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('enabled', enabled)
|
||||
provide('name', name)
|
||||
provide('filename', filename)
|
||||
provide('filepath', filepath)
|
||||
provide('data', data)
|
||||
</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">
|
||||
import type { Site } from '@/api/domain'
|
||||
import type { Site } from '@/api/site'
|
||||
import type { SiteCategory } from '@/api/site_category'
|
||||
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 StdBatchEdit from '@/components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
|
||||
import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.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 SiteDuplicate from '@/views/site/site_list/SiteDuplicate.vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import StdBatchEdit from '../../../components/StdDesign/StdDataDisplay/StdBatchEdit.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
@ -43,7 +43,7 @@ onMounted(async () => {
|
|||
})
|
||||
|
||||
function enable(name: string) {
|
||||
domain.enable(name).then(() => {
|
||||
site.enable(name).then(() => {
|
||||
message.success($gettext('Enabled successfully'))
|
||||
table.value?.get_list()
|
||||
inspect_config.value?.test()
|
||||
|
@ -53,7 +53,7 @@ function enable(name: string) {
|
|||
}
|
||||
|
||||
function disable(name: string) {
|
||||
domain.disable(name).then(() => {
|
||||
site.disable(name).then(() => {
|
||||
message.success($gettext('Disabled successfully'))
|
||||
table.value?.get_list()
|
||||
inspect_config.value?.test()
|
||||
|
@ -63,7 +63,7 @@ function disable(name: string) {
|
|||
}
|
||||
|
||||
function destroy(site_name: string) {
|
||||
domain.destroy(site_name).then(() => {
|
||||
site.destroy(site_name).then(() => {
|
||||
table.value.get_list()
|
||||
message.success($gettext('Delete site: %{site_name}', { site_name }))
|
||||
inspect_config.value?.test()
|
||||
|
@ -104,7 +104,7 @@ function handleBatchUpdated() {
|
|||
|
||||
<StdTable
|
||||
ref="table"
|
||||
:api="domain"
|
||||
:api="site"
|
||||
:columns="columns"
|
||||
row-key="name"
|
||||
disable-delete
|
||||
|
@ -162,7 +162,7 @@ function handleBatchUpdated() {
|
|||
</StdTable>
|
||||
<StdBatchEdit
|
||||
ref="stdBatchEditRef"
|
||||
:api="domain"
|
||||
:api="site"
|
||||
:columns
|
||||
@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-co-op/gocron/v2 v2.12.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/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
|
@ -72,7 +73,7 @@ require (
|
|||
github.com/OpenDNS/vegadns2client v0.0.0-20180418235048-a3fa4a771d87 // indirect
|
||||
github.com/StackExchange/wmi v1.2.1 // 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/config v1.28.0 // 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-playground/locales v0.14.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-viper/mapstructure/v2 v2.2.1 // 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-uuid v1.0.3 // 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/infobloxopen/infoblox-go-client v1.1.1 // 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/sirupsen/logrus v1.9.3 // 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/sony/gobreaker v1.0.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-postgres v0.2.1 // 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/yandex-cloud/go-genproto v0.0.0-20241021132621-28bb61d00c2f // 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-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
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.38/go.mod h1:SOSDHfe1kX91v3W5QiBsWSLqeLxImobbMX1mxrFHsVQ=
|
||||
github.com/aliyun/alibaba-cloud-sdk-go v1.63.39 h1:zlenrBGDiSEu7YnpWiAPscKNolgIo9Z6jvM5pcWAEL4=
|
||||
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/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=
|
||||
|
@ -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/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
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.118/go.mod h1:JWz2ujO9X3oU5wb6kXp+DpR2UuDj2SldDbX8T0FSuhI=
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.119 h1:2pi/hbcuv0CNVcsODkTYZY+X9j5uc1GTjSjX1cWMp/4=
|
||||
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/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=
|
||||
|
@ -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/gunit v1.1.3 h1:32x+htJCu3aMswhPw3teoJ+PnWPONqdNgaGs6Qt8ZaU=
|
||||
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.6/go.mod h1:WeJrBLoTJcaT8nO1azeyHyNpo/fDLtbpbvh+pzts+Qw=
|
||||
github.com/softlayer/softlayer-go v1.1.7 h1:SgTL+pQZt1h+5QkAhVmHORM/7N9c1X0sljJhuOIHxWE=
|
||||
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/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums=
|
||||
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/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/vultr/govultr/v3 v3.11.0 h1:YlAal70AaJ0k848RqcmjAzFcmLS9n8VtPgU68UxvVm8=
|
||||
github.com/vultr/govultr/v3 v3.11.0/go.mod h1:q34Wd76upKmf+vxFMgaNMH3A8BbsPBmSYZUGC8oZa5w=
|
||||
github.com/vultr/govultr/v3 v3.11.1 h1:Wc6wFTwh/gBZlOqSK1Hn3P9JWoFa7NCf52vGLwQcJOg=
|
||||
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/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
|
|
|
@ -2,8 +2,17 @@ package helper
|
|||
|
||||
import "os"
|
||||
|
||||
func FileExists(filename string) bool {
|
||||
_, err := os.Stat(filename)
|
||||
func FileExists(filepath string) bool {
|
||||
_, 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) {
|
||||
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) {
|
||||
err = db.First(&c, &Cert{
|
||||
err = db.Limit(1).Where(&Cert{
|
||||
Filename: confName,
|
||||
}).Error
|
||||
}).Find(&c).Error
|
||||
|
||||
return
|
||||
}
|
||||
|
|
|
@ -2,6 +2,6 @@ package model
|
|||
|
||||
type SiteCategory struct {
|
||||
Model
|
||||
Name string `json:"name"`
|
||||
SyncNodeIds []int `json:"sync_node_ids" gorm:"serializer:json"`
|
||||
Name string `json:"name"`
|
||||
SyncNodeIds []uint64 `json:"sync_node_ids" gorm:"serializer:json"`
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue