feat: config templates #53, #34, #33

This commit is contained in:
0xJacky 2023-01-05 13:46:42 +08:00
parent 594c61a0ff
commit f5a2a634a1
No known key found for this signature in database
GPG key ID: B6E4A6E4A561BAF0
21 changed files with 420 additions and 96 deletions

View file

@ -13,7 +13,7 @@ bin = "tmp/main"
# Customize binary.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
include_ext = ["go", "tpl", "tmpl", "html", "conf"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "upload"]
# Watch these directories if you specified.

View file

@ -28,6 +28,8 @@ declare module '@vue/runtime-core' {
ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
AList: typeof import('ant-design-vue/es')['List']
AListItem: typeof import('ant-design-vue/es')['ListItem']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']

View file

@ -0,0 +1,25 @@
import Curd from '@/api/curd'
import http from '@/lib/http'
class Template extends Curd {
get_config_list() {
return http.get('template/configs')
}
get_block_list() {
return http.get('template/blocks')
}
get_config(name: string) {
return http.get('template/config/' + name)
}
get_block(name: string) {
return http.get('template/block/' + name)
}
}
const template = new Template('/template')
export default template

View file

@ -1,7 +1,6 @@
<script setup lang="ts">
import gettext from '@/gettext'
import {ref, watch} from 'vue'
import {useSettingsStore} from '@/pinia'

View file

@ -39,7 +39,7 @@ const lang = computed(() => {
</script>
<template>
<a-config-provider :locale="lang">
<a-config-provider :locale="lang" :autoInsertSpaceInButton="false">
<a-layout style="min-height: 100%;">
<div class="drawer-sidebar">
<a-drawer

View file

@ -8,6 +8,7 @@ import ngx from '@/api/ngx'
import {computed, reactive, ref} from 'vue'
import {message} from 'ant-design-vue'
import {useRouter} from 'vue-router'
import template from '@/api/template'
const {$gettext, interpolate} = useGettext()
@ -39,7 +40,7 @@ function init() {
function save() {
ngx.build_config(ngx_config).then(r => {
domain.save(config.name, {content: r.content, enabled: true}).then(() => {
domain.save(config.name, {name: config.name, content: r.content, enabled: true}).then(() => {
message.success($gettext('Saved successfully'))
domain.enable(config.name).then(() => {
@ -89,7 +90,6 @@ const has_server_name = computed(() => {
<a-step :title="$gettext('Configure SSL')"/>
<a-step :title="$gettext('Finished')"/>
</a-steps>
<template v-if="current_step===0">
<a-form layout="vertical">
<a-form-item :label="$gettext('Configuration Name')">
@ -97,7 +97,6 @@ const has_server_name = computed(() => {
</a-form-item>
</a-form>
<directive-editor :ngx_directives="ngx_config.servers[0].directives"/>
<br/>
<location-editor :locations="ngx_config.servers[0].locations"/>

View file

@ -166,6 +166,7 @@ const modalClosable = ref(false)
<a-modal
:title="$gettext('Obtaining certificate')"
v-model:visible="modalVisible"
:mask-closable="modalClosable"
:footer="null" :closable="modalClosable" force-render>
<a-progress
:stroke-color="progressStrokeColor"

View file

@ -0,0 +1,106 @@
<script setup lang="ts">
import {useGettext} from 'vue3-gettext'
import template from '@/api/template'
import {computed, ref} from 'vue'
import {storeToRefs} from 'pinia'
import {useSettingsStore} from '@/pinia'
import Template from '@/views/template/Template.vue'
import DirectiveEditor from '@/views/domain/ngx_conf/directive/DirectiveEditor.vue'
import LocationEditor from '@/views/domain/ngx_conf/LocationEditor.vue'
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
const {$gettext} = useGettext()
const {language} = storeToRefs(useSettingsStore())
const props = defineProps(['ngx_config', 'current_server_index'])
const blocks = ref([])
const data: any = ref({})
const visible = ref(false)
function get_block_list() {
template.get_block_list().then(r => {
blocks.value = r.data
})
}
get_block_list()
function view(name: string) {
visible.value = true
template.get_block(name).then(r => {
data.value = r
})
}
const trans_description = computed(() => {
return (item: any) => item.description?.[language.value] ?? item.description?.en ?? ''
})
async function add() {
if (data.value.custom) {
props.ngx_config.custom += '\n' + data.value.custom
}
props.ngx_config.custom = props.ngx_config.custom.trim()
if (data.value.locations) {
props.ngx_config.servers[props.current_server_index].locations.push(...data.value.locations)
}
if (data.value.directives) {
props.ngx_config.servers[props.current_server_index].directives.push(...data.value.directives)
}
visible.value = false
}
</script>
<template>
<div>
<h2 v-translate>Config Templates</h2>
<div class="config-list-wrapper">
<a-list
:grid="{ gutter: 16, xs: 1, sm: 2, md: 2, lg: 2, xl: 2, xxl: 2, xxxl: 2 }"
:data-source="blocks"
>
<template #renderItem="{ item }">
<a-list-item>
<a-card size="small" :title="item.name">
<template #extra>
<a-button type="link" @click="view(item.filename)">View</a-button>
</template>
<p>{{ $gettext('Author') }}: {{ item.author }}</p>
<p>{{ $gettext('Description') }}: {{ trans_description(item) }}</p>
</a-card>
</a-list-item>
</template>
</a-list>
</div>
<a-modal
:title="data.name"
v-model:visible="visible"
:mask="false"
:ok-text="$gettext('Add')"
@ok="add"
>
<p>{{ $gettext('Author') }}: {{ data.author }}</p>
<p>{{ $gettext('Description') }}: {{ trans_description(data) }}</p>
<template v-if="data.custom">
<h2>{{ $gettext('Custom') }}</h2>
<code-editor v-model:content="data.custom" default-height="150px"/>
</template>
<directive-editor v-if="data.directives" :ngx_directives="data.directives" :readonly="true"/>
<br/>
<location-editor v-if="data.locations" :locations="data.locations" :readonly="true"/>
</a-modal>
</div>
</template>
<style lang="less" scoped>
.config-list-wrapper {
max-height: 200px;
overflow-y: scroll;
overflow-x: hidden;
}
</style>

View file

@ -7,7 +7,7 @@ import draggable from 'vuedraggable'
const {$gettext} = useGettext()
const props = defineProps(['locations'])
const props = defineProps(['locations', 'readonly'])
let location = reactive({
comments: '',
@ -52,7 +52,7 @@ function remove(index: number) {
<HolderOutlined/>
{{ $gettext('Location') }}
</template>
<template #extra>
<template #extra v-if="!readonly">
<a-popconfirm @confirm="remove(index)"
:title="$gettext('Are you sure you want to remove this location?')"
:ok-text="$gettext('Yes')"
@ -94,7 +94,7 @@ function remove(index: number) {
</a-form>
</a-modal>
<div>
<div v-if="!readonly">
<a-button block @click="add">{{ $gettext('Add Location') }}</a-button>
</div>
</template>

View file

@ -6,6 +6,8 @@ import {useRoute, useRouter} from 'vue-router'
import {useGettext} from 'vue3-gettext'
import Cert from '@/views/domain/cert/Cert.vue'
import LogEntry from '@/views/domain/ngx_conf/LogEntry.vue'
import ConfigTemplate from '@/views/domain/ngx_conf/ConfigTemplate.vue'
import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
const {$gettext} = useGettext()
@ -151,6 +153,9 @@ watch(current_server_index, () => {
<a-switch @change="change_tls"/>
</a-form-item>
<h2>{{ $gettext('Custom') }}</h2>
<code-editor v-model:content="ngx_config.custom" default-height="150px"/>
<a-tabs v-model:activeKey="current_server_index">
<a-tab-pane :tab="'Server '+(k+1)" v-for="(v,k) in props.ngx_config.servers" :key="k">
<log-entry
@ -175,9 +180,11 @@ watch(current_server_index, () => {
<h3 v-translate>Comments</h3>
<a-textarea v-model:value="v.comments" :bordered="false"/>
</template>
<directive-editor :ngx_directives="v.directives"/>
<br/>
<config-template :ngx_config="ngx_config"
:current_server_index="current_server_index"/>
<br/>
<location-editor :locations="v.locations"/>
</div>

View file

@ -7,9 +7,7 @@ import DirectiveEditorItem from '@/views/domain/ngx_conf/directive/DirectiveEdit
const {$gettext} = useGettext()
const props = defineProps<{
ngx_directives: any[]
}>()
const props = defineProps(['ngx_directives', 'readonly'])
const adding = ref(false)
@ -38,11 +36,13 @@ function onSave(idx: number) {
<directive-editor-item @click="current_idx=index"
:directive="directive"
:current_idx="current_idx" :index="index"
:ngx_directives="ngx_directives"/>
:ngx_directives="ngx_directives"
:readonly="readonly"
/>
</template>
</draggable>
<directive-add :ngx_directives="ngx_directives"/>
<directive-add v-if="!readonly" :ngx_directives="ngx_directives"/>
</template>
<style lang="less" scoped>

View file

@ -10,7 +10,7 @@ import {message} from 'ant-design-vue'
const {$gettext, interpolate} = useGettext()
const props = defineProps(['directive', 'current_idx', 'index', 'ngx_directives'])
const props = defineProps(['directive', 'current_idx', 'index', 'ngx_directives', 'readonly'])
function remove(index: number) {
props.ngx_directives.splice(index, 1)
@ -56,7 +56,8 @@ function save() {
</template>
</a-input>
<a-popconfirm @confirm="remove(index)"
<a-popconfirm v-if="!readonly"
@confirm="remove(index)"
:title="$gettext('Are you sure you want to remove this directive?')"
:ok-text="$gettext('Yes')"
:cancel-text="$gettext('No')">

View file

@ -1,15 +1,11 @@
package api
import (
"bufio"
"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
"github.com/0xJacky/Nginx-UI/server/service"
"github.com/0xJacky/Nginx-UI/server/settings"
"github.com/0xJacky/Nginx-UI/template"
"github.com/gin-gonic/gin"
"io"
"net/http"
"path/filepath"
"regexp"
"strings"
)
@ -64,79 +60,43 @@ proxy_pass http://127.0.0.1:{{ HTTP01PORT }};
}
func GetTemplateConfList(c *gin.Context) {
configs, err := template.DistFS.ReadDir("conf")
configList, err := service.GetTemplateList("conf")
if err != nil {
ErrHandler(c, err)
return
}
type configItem struct {
Name string `json:"name"`
Description map[string]string `json:"description"`
Author string `json:"author"`
}
var configList []configItem
for _, config := range configs {
func() {
configListItem := configItem{
Description: make(map[string]string),
}
file, _ := template.DistFS.Open(filepath.Join("conf", config.Name()))
defer file.Close()
r := bufio.NewReader(file)
bytes, _, err := r.ReadLine()
if err == io.EOF {
return
}
line := strings.TrimSpace(string(bytes))
if line != "# Nginx UI Template Start" {
return
}
var content string
for {
bytes, _, err = r.ReadLine()
if err == io.EOF {
break
}
line = strings.TrimSpace(string(bytes))
if line == "# Nginx UI Template End" {
break
}
content += line + "\n"
}
re := regexp.MustCompile(`# (\S+): (.*)`)
matches := re.FindAllStringSubmatch(content, -1)
for _, match := range matches {
if len(match) < 3 {
continue
}
key := match[1]
switch {
case key == "Name":
configListItem.Name = match[2]
case key == "Author":
configListItem.Author = match[2]
case strings.Contains(key, "Description"):
re = regexp.MustCompile(`(\w+)\[(\w+)\]`)
matches = re.FindAllStringSubmatch(key, -1)
for _, m := range matches {
if len(m) < 3 {
continue
}
// lang => description
configListItem.Description[m[2]] = match[2]
}
}
}
configList = append(configList, configListItem)
}()
}
c.JSON(http.StatusOK, gin.H{
"data": configList,
})
}
func GetTemplateBlockList(c *gin.Context) {
configList, err := service.GetTemplateList("block")
if err != nil {
ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"data": configList,
})
}
func GetTemplateBlock(c *gin.Context) {
type resp struct {
service.ConfigInfoItem
service.ConfigDetail
}
detail, err := service.ParseTemplate("block", c.Param("name"))
if err != nil {
ErrHandler(c, err)
return
}
c.JSON(http.StatusOK, resp{
service.GetTemplateInfo("block", c.Param("name")),
detail,
})
}

View file

@ -13,6 +13,10 @@ const (
Upstream = "upstream"
)
func (s *NgxServer) ParseServer(directive gonginx.IDirective) {
s.parseServer(directive)
}
func (s *NgxServer) parseServer(directive gonginx.IDirective) {
if directive.GetBlock() == nil {
return
@ -36,7 +40,9 @@ func (s *NgxServer) parseServer(directive gonginx.IDirective) {
}
}
}
func (l *NgxLocation) ParseLocation(directive gonginx.IDirective, deep int) {
l.parseLocation(directive, deep)
}
func (l *NgxLocation) parseLocation(directive gonginx.IDirective, deep int) {
if directive.GetBlock() == nil {
return
@ -52,6 +58,10 @@ func (l *NgxLocation) parseLocation(directive gonginx.IDirective, deep int) {
}
}
func (d *NgxDirective) ParseDirective(directive gonginx.IDirective, deep int) {
d.parseDirective(directive, deep)
}
func (d *NgxDirective) parseDirective(directive gonginx.IDirective, deep int) {
if directive.GetBlock() != nil {
d.Params += directive.GetName() + " "
@ -134,6 +144,7 @@ func parse(block gonginx.IBlock, ngxConfig *NgxConfig) {
ngxConfig.parseCustom(v)
}
}
ngxConfig.Custom = FmtCode(ngxConfig.Custom)
}
func ParseNgxConfigByContent(content string) (ngxConfig *NgxConfig) {

View file

@ -77,6 +77,8 @@ func InitRouter() *gin.Engine {
g.GET("template", api.GetTemplate)
g.GET("template/configs", api.GetTemplateConfList)
g.GET("template/blocks", api.GetTemplateBlockList)
g.GET("template/block/:name", api.GetTemplateBlock)
g.GET("cert/issue", api.IssueCert)

145
server/service/template.go Normal file
View file

@ -0,0 +1,145 @@
package service
import (
"bufio"
"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
"github.com/0xJacky/Nginx-UI/template"
"github.com/pkg/errors"
"github.com/tufanbarisyildirim/gonginx/parser"
"io"
"path/filepath"
"regexp"
"strings"
)
type ConfigInfoItem struct {
Name string `json:"name"`
Description map[string]string `json:"description"`
Author string `json:"author"`
Filename string `json:"filename"`
}
func GetTemplateInfo(path, name string) (configListItem ConfigInfoItem) {
configListItem = ConfigInfoItem{
Description: make(map[string]string),
Filename: name,
}
file, _ := template.DistFS.Open(filepath.Join(path, name))
defer file.Close()
r := bufio.NewReader(file)
bytes, _, err := r.ReadLine()
if err == io.EOF {
return
}
line := strings.TrimSpace(string(bytes))
if line != "# Nginx UI Template Start" {
return
}
var content string
for {
bytes, _, err = r.ReadLine()
if err == io.EOF {
break
}
line = strings.TrimSpace(string(bytes))
if line == "# Nginx UI Template End" {
break
}
content += line + "\n"
}
re := regexp.MustCompile(`# (\S+): (.*)`)
matches := re.FindAllStringSubmatch(content, -1)
for _, match := range matches {
if len(match) < 3 {
continue
}
key := match[1]
switch {
case key == "Name":
configListItem.Name = match[2]
case key == "Author":
configListItem.Author = match[2]
case strings.Contains(key, "Description"):
re = regexp.MustCompile(`(\w+)\[(\w+)\]`)
matches = re.FindAllStringSubmatch(key, -1)
for _, m := range matches {
if len(m) < 3 {
continue
}
// lang => description
configListItem.Description[m[2]] = match[2]
}
}
}
return
}
type ConfigDetail struct {
Custom string `json:"custom"`
nginx.NgxServer
}
func ParseTemplate(path, name string) (c ConfigDetail, err error) {
file, err := template.DistFS.Open(filepath.Join(path, name))
if err != nil {
err = errors.Wrap(err, "error tokenized template")
return
}
defer file.Close()
r := bufio.NewReader(file)
var flag bool
custom := ""
content := ""
for {
bytes, _, err := r.ReadLine()
if err == io.EOF {
break
}
orig := string(bytes)
line := strings.TrimSpace(orig)
switch {
case line == "# Nginx UI Custom Start":
flag = true
case line == "# Nginx UI Custom End":
flag = false
case flag == true:
custom += orig + "\n"
case flag == false:
content += orig + "\n"
}
}
p := parser.NewStringParser(content)
config := p.Parse()
c.Custom = custom
for _, d := range config.GetDirectives() {
switch d.GetName() {
case nginx.Location:
l := &nginx.NgxLocation{}
l.ParseLocation(d, 0)
c.NgxServer.Locations = append(c.NgxServer.Locations, l)
default:
dir := &nginx.NgxDirective{}
dir.ParseDirective(d, 0)
c.NgxServer.Directives = append(c.NgxServer.Directives, dir)
}
}
return
}
func GetTemplateList(path string) (configList []ConfigInfoItem, err error) {
configs, err := template.DistFS.ReadDir(path)
if err != nil {
err = errors.Wrap(err, "error get template list")
return
}
for _, config := range configs {
configList = append(configList, GetTemplateInfo(path, config.Name()))
}
return
}

View file

@ -1,3 +1,9 @@
location / {
try_files $uri $uri/ /index.php;
}
# Nginx UI Template Start
# Name: Codeigniter Rewrite
# Description[en]: Codeigniter URL Rewrite Config
# Description[zh_CN]: Codeigniter 伪静态配置
# Author: @0xJacky
# Nginx UI Template End
location / {
try_files $uri $uri/ /index.php;
}

View file

@ -0,0 +1,13 @@
# Nginx UI Template Start
# Name: PHP8.1
# Description[en]: Enabled PHP 8.1 Config
# Description[zh_CN]: 启用 PHP 8.1 配置
# Author: @0xJacky
# Nginx UI Template End
location ~ [^/]\.php(/|$)
{
try_files $uri =404;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
include fastcgi.conf;
}

View file

@ -1,3 +1,9 @@
location / {
try_files $uri $uri/ /server.php?$query_string;
}
# Nginx UI Template Start
# Name: Laravel Rewrite
# Description[en]: Laravel URL Rewrite Config
# Description[zh_CN]: Laravel 伪静态配置
# Author: @0xJacky
# Nginx UI Template End
location / {
try_files $uri $uri/ /server.php?$query_string;
}

View file

@ -0,0 +1,15 @@
# Nginx UI Template Start
# Name: Reverse Proxy
# Description[en]: Reverse Proxy Config
# Description[zh_CN]: 反向代理配置
# Author: @0xJacky
# Nginx UI Template End
location / {
proxy_pass http://127.0.0.1:9000/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 1000m;
}

View file

@ -0,0 +1,26 @@
# Nginx UI Template Start
# Name: Reverse Proxy WebSocket
# Description[en]: Reverse Proxy with WebSocket Config
# Description[zh_CN]: 反向代理 WebSocket 配置
# Author: @0xJacky
# Nginx UI Template End
# Nginx UI Custom Start
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
# Nginx UI Custom End
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:9000/;
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 1000m;
}