refactor: config management

This commit is contained in:
Jacky 2024-07-25 18:19:42 +08:00
parent eb9ede5a4e
commit 53ae1a1ef9
No known key found for this signature in database
GPG key ID: 215C21B10DF38B4D
12 changed files with 405 additions and 203 deletions

View file

@ -3,43 +3,49 @@ package config
import (
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai"
"net/http"
"os"
"time"
)
func AddConfig(c *gin.Context) {
var request struct {
var json struct {
Name string `json:"name" binding:"required"`
Content string `json:"content" binding:"required"`
NewFilepath string `json:"new_filepath" binding:"required"`
Content string `json:"content"`
Overwrite bool `json:"overwrite"`
}
err := c.BindJSON(&request)
if err != nil {
api.ErrHandler(c, err)
if !api.BindAndValid(c, &json) {
return
}
name := request.Name
content := request.Content
path := nginx.GetConfPath("/", name)
if _, err = os.Stat(path); err == nil {
c.JSON(http.StatusNotAcceptable, gin.H{
"message": "config exist",
name := json.Name
content := json.Content
path := json.NewFilepath
if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
c.JSON(http.StatusForbidden, gin.H{
"message": "new filepath is not under the nginx conf path",
})
return
}
if content != "" {
err = os.WriteFile(path, []byte(content), 0644)
if !json.Overwrite && helper.FileExists(path) {
c.JSON(http.StatusNotAcceptable, gin.H{
"message": "File exists",
})
return
}
err := os.WriteFile(path, []byte(content), 0644)
if err != nil {
api.ErrHandler(c, err)
return
}
}
output := nginx.Reload()
if nginx.GetLogLevel(output) >= nginx.Warn {
@ -52,5 +58,8 @@ func AddConfig(c *gin.Context) {
c.JSON(http.StatusOK, config.Config{
Name: name,
Content: content,
ChatGPTMessages: make([]openai.ChatCompletionMessage, 0),
FilePath: path,
ModifiedAt: time.Now(),
})
}

13
api/config/base_path.go Normal file
View file

@ -0,0 +1,13 @@
package config
import (
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/gin-gonic/gin"
"net/http"
)
func GetBasePath(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"base_path": nginx.GetConfPath(),
})
}

View file

@ -3,6 +3,7 @@ package config
import (
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
@ -15,16 +16,20 @@ func GetConfig(c *gin.Context) {
name := c.Param("name")
path := nginx.GetConfPath("/", name)
if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
c.JSON(http.StatusForbidden, gin.H{
"message": "path is not under the nginx conf path",
})
return
}
stat, err := os.Stat(path)
if err != nil {
api.ErrHandler(c, err)
return
}
content, err := os.ReadFile(path)
if err != nil {
api.ErrHandler(c, err)
return
@ -32,7 +37,6 @@ func GetConfig(c *gin.Context) {
g := query.ChatGPTLog
chatgpt, err := g.Where(g.Name.Eq(path)).FirstOrCreate()
if err != nil {
api.ErrHandler(c, err)
return

View file

@ -2,10 +2,15 @@ package config
import (
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/internal/config"
"github.com/0xJacky/Nginx-UI/internal/helper"
"github.com/0xJacky/Nginx-UI/internal/nginx"
"github.com/0xJacky/Nginx-UI/query"
"github.com/gin-gonic/gin"
"github.com/sashabaranov/go-openai"
"net/http"
"os"
"time"
)
type EditConfigJson struct {
@ -14,15 +19,39 @@ type EditConfigJson struct {
func EditConfig(c *gin.Context) {
name := c.Param("name")
var request EditConfigJson
err := c.BindJSON(&request)
if err != nil {
api.ErrHandler(c, err)
var json struct {
Name string `json:"name" binding:"required"`
Filepath string `json:"filepath" binding:"required"`
NewFilepath string `json:"new_filepath" binding:"required"`
Content string `json:"content"`
}
if !api.BindAndValid(c, &json) {
return
}
path := nginx.GetConfPath("/", name)
content := request.Content
path := json.Filepath
if !helper.IsUnderDirectory(path, nginx.GetConfPath()) {
c.JSON(http.StatusForbidden, gin.H{
"message": "filepath is not under the nginx conf path",
})
return
}
if !helper.IsUnderDirectory(json.NewFilepath, nginx.GetConfPath()) {
c.JSON(http.StatusForbidden, gin.H{
"message": "new filepath is not under the nginx conf path",
})
return
}
if _, err := os.Stat(path); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, gin.H{
"message": "file not found",
})
return
}
content := json.Content
origContent, err := os.ReadFile(path)
if err != nil {
api.ErrHandler(c, err)
@ -37,8 +66,28 @@ func EditConfig(c *gin.Context) {
}
}
output := nginx.Reload()
g := query.ChatGPTLog
// handle rename
if path != json.NewFilepath {
if helper.FileExists(json.NewFilepath) {
c.JSON(http.StatusNotAcceptable, gin.H{
"message": "File exists",
})
return
}
err := os.Rename(json.Filepath, json.NewFilepath)
if err != nil {
api.ErrHandler(c, err)
return
}
// update ChatGPT record
_, _ = g.Where(g.Name.Eq(json.NewFilepath)).Delete()
_, _ = g.Where(g.Name.Eq(path)).Update(g.Name, json.NewFilepath)
}
output := nginx.Reload()
if nginx.GetLogLevel(output) >= nginx.Warn {
c.JSON(http.StatusInternalServerError, gin.H{
"message": output,
@ -46,5 +95,21 @@ func EditConfig(c *gin.Context) {
return
}
GetConfig(c)
chatgpt, err := g.Where(g.Name.Eq(json.NewFilepath)).FirstOrCreate()
if err != nil {
api.ErrHandler(c, err)
return
}
if chatgpt.Content == nil {
chatgpt.Content = make([]openai.ChatCompletionMessage, 0)
}
c.JSON(http.StatusOK, config.Config{
Name: name,
Content: content,
ChatGPTMessages: chatgpt.Content,
FilePath: json.NewFilepath,
ModifiedAt: time.Now(),
})
}

View file

@ -7,4 +7,5 @@ func InitRouter(r *gin.RouterGroup) {
r.GET("config/*name", GetConfig)
r.POST("config", AddConfig)
r.POST("config/*name", EditConfig)
r.GET("config_base_path", GetBasePath)
}

View file

@ -1,5 +1,6 @@
import Curd from '@/api/curd'
import type { ChatComplicationMessage } from '@/api/openai'
import http from '@/lib/http'
export interface Config {
name: string
@ -9,6 +10,16 @@ export interface Config {
modified_at: string
}
const config: Curd<Config> = new Curd('/config')
class ConfigCurd extends Curd<Config> {
constructor() {
super('/config')
}
get_base_path() {
return http.get('/config_base_path')
}
}
const config: ConfigCurd = new ConfigCurd()
export default config

View file

@ -104,13 +104,24 @@ export const routes: RouteRecordRaw[] = [
hideChildren: true,
},
},
{
path: 'config/add',
name: 'Add Configuration',
component: () => import('@/views/config/ConfigEditor.vue'),
meta: {
name: () => $gettext('Add Configuration'),
hiddenInSidebar: true,
lastRouteName: 'Manage Configs',
},
},
{
path: 'config/:name+/edit',
name: 'Edit Configuration',
component: () => import('@/views/config/ConfigEdit.vue'),
component: () => import('@/views/config/ConfigEditor.vue'),
meta: {
name: () => $gettext('Edit Configuration'),
hiddenInSidebar: true,
lastRouteName: 'Manage Configs',
},
},
{

View file

@ -3,20 +3,18 @@ import StdTable from '@/components/StdDesign/StdDataDisplay/StdTable.vue'
import config from '@/api/config'
import configColumns from '@/views/config/config'
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
import router from '@/routes'
import InspectConfig from '@/views/config/InspectConfig.vue'
const api = config
const table = ref(null)
const table = ref()
const route = useRoute()
const router = useRouter()
const basePath = computed(() => {
let dir = route?.query?.dir ?? ''
if (dir)
dir += '/'
return dir
return dir as string
})
const getParams = computed(() => {
@ -36,15 +34,32 @@ const refInspectConfig = ref()
watch(route, () => {
refInspectConfig.value?.test()
})
function goBack() {
router.push({
path: '/config',
query: {
dir: `${basePath.value.split('/').slice(0, -2).join('/')}` || undefined,
},
})
}
</script>
<template>
<ACard :title="$gettext('Configurations')">
<template #extra>
<a
@click="router.push({
path: '/config/add',
query: { basePath: basePath || undefined },
})"
>{{ $gettext('Add') }}</a>
</template>
<InspectConfig ref="refInspectConfig" />
<StdTable
:key="update"
ref="table"
:api="api"
:api="config"
:columns="configColumns"
disable-delete
disable-search
@ -68,7 +83,7 @@ watch(route, () => {
}"
/>
<FooterToolBar v-if="basePath">
<AButton @click="router.go(-1)">
<AButton @click="goBack">
{{ $gettext('Back') }}
</AButton>
</FooterToolBar>

View file

@ -1,160 +0,0 @@
<script setup lang="ts">
import { useRoute } from 'vue-router'
import { message } from 'ant-design-vue'
import type { Ref } from 'vue'
import { formatDateTime } from '@/lib/helper'
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
import config from '@/api/config'
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
import ngx from '@/api/ngx'
import InspectConfig from '@/views/config/InspectConfig.vue'
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
import type { ChatComplicationMessage } from '@/api/openai'
const route = useRoute()
const inspect_config = ref()
const name = computed(() => {
const n = route.params.name
if (typeof n === 'string')
return n
return n?.join('/')
})
const configText = ref('')
const history_chatgpt_record = ref([]) as Ref<ChatComplicationMessage[]>
const filepath = ref('')
const active_key = ref(['1', '2'])
const modified_at = ref('')
function init() {
if (name.value) {
config.get(name.value).then(r => {
configText.value = r.content
history_chatgpt_record.value = r.chatgpt_messages
filepath.value = r.filepath
modified_at.value = r.modified_at
}).catch(r => {
message.error(r.message ?? $gettext('Server error'))
})
}
else {
configText.value = ''
history_chatgpt_record.value = []
filepath.value = ''
}
}
init()
function save() {
config.save(name.value, { content: configText.value }).then(r => {
configText.value = r.content
message.success($gettext('Saved successfully'))
}).catch(r => {
message.error($gettext('Save error %{msg}', { msg: r.message ?? '' }))
}).finally(() => {
inspect_config.value.test()
})
}
function format_code() {
ngx.format_code(configText.value).then(r => {
configText.value = r.content
message.success($gettext('Format successfully'))
}).catch(r => {
message.error($gettext('Format error %{msg}', { msg: r.message ?? '' }))
})
}
</script>
<template>
<ARow :gutter="16">
<ACol
:xs="24"
:sm="24"
:md="18"
>
<ACard :title="$gettext('Edit Configuration')">
<InspectConfig ref="inspect_config" />
<CodeEditor v-model:content="configText" />
<FooterToolBar>
<ASpace>
<AButton @click="$router.go(-1)">
{{ $gettext('Back') }}
</AButton>
<AButton @click="format_code">
{{ $gettext('Format Code') }}
</AButton>
<AButton
type="primary"
@click="save"
>
{{ $gettext('Save') }}
</AButton>
</ASpace>
</FooterToolBar>
</ACard>
</ACol>
<ACol
:xs="24"
:sm="24"
:md="6"
>
<ACard class="col-right">
<ACollapse
v-model:activeKey="active_key"
ghost
>
<ACollapsePanel
key="1"
:header="$gettext('Basic')"
>
<AForm layout="vertical">
<AFormItem :label="$gettext('Path')">
{{ filepath }}
</AFormItem>
<AFormItem :label="$gettext('Updated at')">
{{ formatDateTime(modified_at) }}
</AFormItem>
</AForm>
</ACollapsePanel>
<ACollapsePanel
key="2"
header="ChatGPT"
>
<ChatGPT
v-model:history-messages="history_chatgpt_record"
:content="configText"
:path="filepath"
/>
</ACollapsePanel>
</ACollapse>
</ACard>
</ACol>
</ARow>
</template>
<style lang="less" scoped>
.col-right {
position: sticky;
top: 78px;
:deep(.ant-card-body) {
max-height: 100vh;
overflow-y: scroll;
}
}
:deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
padding: 0;
}
:deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
padding: 0 0 10px 0;
}
</style>

View file

@ -0,0 +1,232 @@
<script setup lang="ts">
import { message } from 'ant-design-vue'
import type { Ref } from 'vue'
import { formatDateTime } from '@/lib/helper'
import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
import config from '@/api/config'
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
import ngx from '@/api/ngx'
import InspectConfig from '@/views/config/InspectConfig.vue'
import ChatGPT from '@/components/ChatGPT/ChatGPT.vue'
import type { ChatComplicationMessage } from '@/api/openai'
const route = useRoute()
const router = useRouter()
const refForm = ref()
const refInspectConfig = ref()
const origName = ref('')
const addMode = computed(() => !route.params.name)
const errors = ref({})
const basePath = computed(() => {
if (route.query.basePath)
return route?.query?.basePath?.toString().replaceAll('/', '')
else if (typeof route.params.name === 'object')
return (route.params.name as string[]).slice(0, -1).join('/')
else
return ''
})
const data = ref({
name: '',
content: '',
filepath: '',
})
const historyChatgptRecord = ref([]) as Ref<ChatComplicationMessage[]>
const activeKey = ref(['basic', 'chatgpt'])
const modifiedAt = ref('')
const nginxConfigBase = ref('')
const newPath = computed(() => [nginxConfigBase.value, basePath.value, data.value.name]
.filter(v => v)
.join('/'))
const relativePath = computed(() => (route.params.name as string[]).join('/'))
async function init() {
const { name } = route.params
data.value.name = name?.[name?.length - 1] ?? ''
origName.value = data.value.name
if (data.value.name) {
config.get(relativePath.value).then(r => {
data.value = r
historyChatgptRecord.value = r.chatgpt_messages
modifiedAt.value = r.modified_at
}).catch(r => {
message.error(r.message ?? $gettext('Server error'))
})
}
else {
data.value.content = ''
historyChatgptRecord.value = []
data.value.filepath = ''
}
}
onMounted(async () => {
await config.get_base_path().then(r => {
nginxConfigBase.value = r.base_path
})
await init()
})
function save() {
refForm.value.validate().then(() => {
config.save(addMode.value ? null : relativePath.value, {
name: data.value.name,
filepath: data.value.filepath,
new_filepath: newPath.value,
content: data.value.content,
}).then(r => {
data.value.content = r.content
message.success($gettext('Saved successfully'))
router.push(`/config/${r.filepath.replaceAll(`${nginxConfigBase.value}/`, '')}/edit`)
}).catch(e => {
errors.value = e.errors
message.error($gettext('Save error %{msg}', { msg: e.message ?? '' }))
}).finally(() => {
refInspectConfig.value.test()
})
})
}
function formatCode() {
ngx.format_code(data.value.content).then(r => {
data.value.content = r.content
message.success($gettext('Format successfully'))
}).catch(r => {
message.error($gettext('Format error %{msg}', { msg: r.message ?? '' }))
})
}
function goBack() {
router.push({
path: '/config',
query: {
dir: basePath.value || undefined,
},
})
}
</script>
<template>
<ARow :gutter="16">
<ACol
:xs="24"
:sm="24"
:md="18"
>
<ACard :title="addMode ? $gettext('Add Configuration') : $gettext('Edit Configuration')">
<InspectConfig
v-show="!addMode"
ref="refInspectConfig"
/>
<CodeEditor v-model:content="data.content" />
<FooterToolBar>
<ASpace>
<AButton @click="goBack">
{{ $gettext('Back') }}
</AButton>
<AButton @click="formatCode">
{{ $gettext('Format Code') }}
</AButton>
<AButton
type="primary"
@click="save"
>
{{ $gettext('Save') }}
</AButton>
</ASpace>
</FooterToolBar>
</ACard>
</ACol>
<ACol
:xs="24"
:sm="24"
:md="6"
>
<ACard class="col-right">
<ACollapse
v-model:activeKey="activeKey"
ghost
>
<ACollapsePanel
key="basic"
:header="$gettext('Basic')"
>
<AForm
ref="refForm"
layout="vertical"
:model="data"
:rules="{
name: [
{ required: true, message: $gettext('Please input a filename') },
{ pattern: /^[^\\/]+$/, message: $gettext('Invalid filename') },
],
}"
>
<AFormItem
name="name"
:label="$gettext('Name')"
>
<AInput v-model:value="data.name" />
</AFormItem>
<AFormItem
v-if="!addMode"
:label="$gettext('Path')"
>
{{ data.filepath }}
</AFormItem>
<AFormItem
v-show="data.name !== origName"
:label="addMode ? $gettext('New Path') : $gettext('Changed Path')"
required
>
{{ newPath }}
</AFormItem>
<AFormItem
v-if="!addMode"
:label="$gettext('Updated at')"
>
{{ formatDateTime(modifiedAt) }}
</AFormItem>
</AForm>
</ACollapsePanel>
<ACollapsePanel
key="chatgpt"
header="ChatGPT"
>
<ChatGPT
v-model:history-messages="historyChatgptRecord"
:content="data.content"
:path="data.filepath"
/>
</ACollapsePanel>
</ACollapse>
</ACard>
</ACol>
</ARow>
</template>
<style lang="less" scoped>
.col-right {
position: sticky;
top: 78px;
:deep(.ant-card-body) {
max-height: 100vh;
overflow-y: scroll;
}
}
:deep(.ant-collapse-ghost > .ant-collapse-item > .ant-collapse-content > .ant-collapse-content-box) {
padding: 0;
}
:deep(.ant-collapse > .ant-collapse-item > .ant-collapse-header) {
padding: 0 0 10px 0;
}
</style>

View file

@ -7,7 +7,7 @@ import (
type Config struct {
Name string `json:"name"`
Content string `json:"content,omitempty"`
Content string `json:"content"`
ChatGPTMessages []openai.ChatCompletionMessage `json:"chatgpt_messages,omitempty"`
FilePath string `json:"filepath,omitempty"`
ModifiedAt time.Time `json:"modified_at"`

View file

@ -8,4 +8,5 @@ import (
func TestIsUnderDirectory(t *testing.T) {
assert.Equal(t, true, IsUnderDirectory("/etc/nginx/nginx.conf", "/etc/nginx"))
assert.Equal(t, false, IsUnderDirectory("../../root/nginx.conf", "/etc/nginx"))
assert.Equal(t, false, IsUnderDirectory("/etc/nginx/../../root/nginx.conf", "/etc/nginx"))
}