Selaa lähdekoodia

Merge pull request #55 from 0xJacky/1.7-dev

v1.7
Jacky 2 vuotta sitten
vanhempi
commit
adb6f888d2
89 muutettua tiedostoa jossa 3734 lisäystä ja 1850 poistoa
  1. 1 1
      .air.toml
  2. 9 5
      README-zh_CN.md
  3. 9 5
      README-zh_TW.md
  4. 1 0
      README.md
  5. 2 3
      frontend/components.d.ts
  6. 2 2
      frontend/package.json
  7. 11 6
      frontend/src/App.vue
  8. 5 0
      frontend/src/api/cert.ts
  9. 3 3
      frontend/src/api/domain.ts
  10. 4 0
      frontend/src/api/ngx.ts
  11. 12 0
      frontend/src/api/settings.ts
  12. 25 0
      frontend/src/api/template.ts
  13. 3 6
      frontend/src/components/CodeEditor/CodeEditor.vue
  14. 7 4
      frontend/src/components/FooterToolbar/FooterToolBar.vue
  15. 8 5
      frontend/src/components/Logo/Logo.vue
  16. 8 5
      frontend/src/components/PageHeader/PageHeader.vue
  17. 0 1
      frontend/src/components/SetLanguage/SetLanguage.vue
  18. 21 26
      frontend/src/components/StdDataEntry/components/StdSelector.vue
  19. 0 51
      frontend/src/components/StdDataEntry/compontents/StdPassword.vue
  20. 0 45
      frontend/src/components/StdDataEntry/compontents/StdSelect.vue
  21. 0 137
      frontend/src/components/StdDataEntry/compontents/StdSelector.vue
  22. 356 131
      frontend/src/language/en/app.po
  23. 357 137
      frontend/src/language/messages.pot
  24. 0 0
      frontend/src/language/translations.json
  25. BIN
      frontend/src/language/zh_CN/app.mo
  26. 343 137
      frontend/src/language/zh_CN/app.po
  27. BIN
      frontend/src/language/zh_TW/app.mo
  28. 391 178
      frontend/src/language/zh_TW/app.po
  29. 32 28
      frontend/src/layouts/BaseLayout.vue
  30. 1 1
      frontend/src/layouts/HeaderLayout.vue
  31. 1 0
      frontend/src/lib/theme/index.ts
  32. 4 0
      frontend/src/pinia/moudule/settings.ts
  33. 30 12
      frontend/src/routes/index.ts
  34. 1 1
      frontend/src/version.json
  35. 114 0
      frontend/src/views/cert/Cert.vue
  36. 47 22
      frontend/src/views/config/Config.vue
  37. 21 3
      frontend/src/views/config/ConfigEdit.vue
  38. 40 0
      frontend/src/views/config/config.ts
  39. 2 3
      frontend/src/views/domain/DomainAdd.vue
  40. 14 5
      frontend/src/views/domain/DomainEdit.vue
  41. 2 2
      frontend/src/views/domain/DomainList.vue
  42. 7 0
      frontend/src/views/domain/cert/Cert.vue
  43. 0 5
      frontend/src/views/domain/cert/CertInfo.vue
  44. 90 0
      frontend/src/views/domain/cert/ChangeCert.vue
  45. 31 40
      frontend/src/views/domain/cert/IssueCert.vue
  46. 106 0
      frontend/src/views/domain/ngx_conf/ConfigTemplate.vue
  47. 3 3
      frontend/src/views/domain/ngx_conf/LocationEditor.vue
  48. 8 1
      frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue
  49. 9 7
      frontend/src/views/domain/ngx_conf/directive/DirectiveAdd.vue
  50. 10 82
      frontend/src/views/domain/ngx_conf/directive/DirectiveEditor.vue
  51. 138 0
      frontend/src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue
  52. 2 2
      frontend/src/views/nginx_log/NginxLog.vue
  53. 101 0
      frontend/src/views/preference/Preference.vue
  54. 11 0
      frontend/src/views/template/Template.vue
  55. 1 1
      frontend/version.json
  56. 149 149
      frontend/yarn.lock
  57. 2 2
      go.mod
  58. 17 3
      go.sum
  59. 310 98
      server/api/cert.go
  60. 4 4
      server/api/config.go
  61. 82 11
      server/api/domain.go
  62. 0 1
      server/api/nginx_log.go
  63. 31 26
      server/api/ngx.go
  64. 38 0
      server/api/settings.go
  65. 43 0
      server/api/template.go
  66. 29 3
      server/model/cert.go
  67. 3 1
      server/pkg/cert/auto_cert.go
  68. 10 4
      server/pkg/cert/cert.go
  69. 70 68
      server/pkg/nginx/build_config.go
  70. 0 0
      server/pkg/nginx/conf/nextcloud_ngx.conf
  71. 36 0
      server/pkg/nginx/conf/test.conf
  72. 10 41
      server/pkg/nginx/format_code.go
  73. 49 0
      server/pkg/nginx/ngx_conf_parse_test.go
  74. 128 109
      server/pkg/nginx/parse.go
  75. 0 125
      server/pkg/nginx/tokenize.go
  76. 13 32
      server/pkg/nginx/type.go
  77. 19 13
      server/router/routers.go
  78. 149 0
      server/service/template.go
  79. 13 12
      server/settings/settings.go
  80. 0 42
      server/test/ngx_conf_parse_test.go
  81. 9 0
      template/block/codeigniter.conf
  82. 13 0
      template/block/enable-php-8.conf
  83. 9 0
      template/block/laravel.conf
  84. 23 0
      template/block/nginx-ui.conf
  85. 15 0
      template/block/reverse_proxy.conf
  86. 26 0
      template/block/reverse_proxy_ws.conf
  87. 12 0
      template/block/wordpress.conf
  88. 12 0
      template/conf/wordpress.conf
  89. 6 0
      template/template.go

+ 1 - 1
.air.toml

@@ -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.

+ 9 - 5
README-zh_CN.md

@@ -91,12 +91,16 @@ Nginx 网络管理界面,由  [0xJacky](https://jackyu.cn/) 与 [Hintay](https
 我们欢迎您将项目翻译成任何语言。
 
 ### 构建基于
-
-- [The Go Programming Language](https://go.dev/)
+- [The Go Programming Language](https://go.dev)
 - [Gin Web Framework](https://gin-gonic.com)
-- [GORM](http://gorm.io/index.html)
-- [Vue 2](https://vuejs.org)
-- [vue-gettext](https://github.com/Polyconseil/vue-gettext)
+- [GORM](http://gorm.io)
+- [Vue 3](https://v3.vuejs.org)
+- [Vite](https://vitejs.dev)
+- [TypeScript](https://www.typescriptlang.org/)
+- [Ant Design Vue](https://antdv.com)
+- [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
+- [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
+- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
 
 ## 入门指南
 

+ 9 - 5
README-zh_TW.md

@@ -93,12 +93,16 @@ Nginx 網路管理介面,由  [0xJacky](https://jackyu.cn/) 與 [Hintay](https
 我們歡迎您將專案翻譯成任何語言。
 
 ### 構建基於
-
-- [The Go Programming Language](https://go.dev/)
+- [The Go Programming Language](https://go.dev)
 - [Gin Web Framework](https://gin-gonic.com)
-- [GORM](http://gorm.io/index.html)
-- [Vue 2](https://vuejs.org)
-- [vue-gettext](https://github.com/Polyconseil/vue-gettext)
+- [GORM](http://gorm.io)
+- [Vue 3](https://v3.vuejs.org)
+- [Vite](https://vitejs.dev)
+- [TypeScript](https://www.typescriptlang.org/)
+- [Ant Design Vue](https://antdv.com)
+- [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
+- [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
+- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
 
 ## 入門指南
 

+ 1 - 0
README.md

@@ -99,6 +99,7 @@ We welcome translations into any language.
 - [Ant Design Vue](https://antdv.com)
 - [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
 - [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
+- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
 
 ## Getting Started
 

+ 2 - 3
frontend/components.d.ts

@@ -27,6 +27,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']
@@ -65,8 +67,5 @@ declare module '@vue/runtime-core' {
     StdDataEntryComponentsStdPassword: typeof import('./src/components/StdDataEntry/components/StdPassword.vue')['default']
     StdDataEntryComponentsStdSelect: typeof import('./src/components/StdDataEntry/components/StdSelect.vue')['default']
     StdDataEntryComponentsStdSelector: typeof import('./src/components/StdDataEntry/components/StdSelector.vue')['default']
-    StdDataEntryCompontentsStdPassword: typeof import('./src/components/StdDataEntry/compontents/StdPassword.vue')['default']
-    StdDataEntryCompontentsStdSelect: typeof import('./src/components/StdDataEntry/compontents/StdSelect.vue')['default']
-    StdDataEntryCompontentsStdSelector: typeof import('./src/components/StdDataEntry/compontents/StdSelector.vue')['default']
   }
 }

+ 2 - 2
frontend/package.json

@@ -1,7 +1,7 @@
 {
     "name": "nginx-ui-frontend-next",
     "private": true,
-    "version": "1.6.8",
+    "version": "1.7.0",
     "type": "commonjs",
     "scripts": {
         "dev": "vite",
@@ -39,7 +39,7 @@
         "less": "^4.1.3",
         "typescript": "^4.6.4",
         "unplugin-vue-components": "^0.22.9",
-        "vite": "^3.2.3",
+        "vite": "^4.0.3",
         "vite-plugin-html": "^3.2.0",
         "vue-tsc": "^1.0.9"
     }

+ 11 - 6
frontend/src/App.vue

@@ -5,17 +5,22 @@ import {useSettingsStore} from '@/pinia'
 import {dark_mode} from '@/lib/theme'
 
 let media = window.matchMedia('(prefers-color-scheme: dark)')
+
 const callback = (media: { matches: any; }) => {
     const settings = useSettingsStore()
-    if (media.matches) {
-        dark_mode(true)
-        settings.set_theme('dark')
-    } else {
-        dark_mode(false)
-        settings.set_theme('default')
+    if (settings.preference_theme === 'auto') {
+        if (media.matches) {
+            dark_mode(true)
+            settings.set_theme('dark')
+        } else {
+            dark_mode(false)
+            settings.set_theme('auto')
+        }
     }
 }
+
 callback(media)
+
 if (typeof media.addEventListener === 'function') {
     media.addEventListener('change', callback)
 } else if (typeof media.addListener === 'function') {

+ 5 - 0
frontend/src/api/cert.ts

@@ -0,0 +1,5 @@
+import Curd from '@/api/curd'
+
+const cert = new Curd('/cert')
+
+export default cert

+ 3 - 3
frontend/src/api/domain.ts

@@ -13,13 +13,13 @@ class Domain extends Curd {
     get_template() {
         return http.get('template')
     }
-    
+
     add_auto_cert(domain: string) {
-        return http.post('cert/' + domain)
+        return http.post('auto_cert/' + domain)
     }
 
     remove_auto_cert(domain: string) {
-        return http.delete('cert/' + domain)
+        return http.delete('auto_cert/' + domain)
     }
 }
 

+ 4 - 0
frontend/src/api/ngx.ts

@@ -7,6 +7,10 @@ const ngx = {
 
     tokenize_config(content: string) {
         return http.post('/ngx/tokenize_config', {content})
+    },
+
+    format_code(content: string) {
+        return http.post('/ngx/format_code', {content})
     }
 }
 

+ 12 - 0
frontend/src/api/settings.ts

@@ -0,0 +1,12 @@
+import http from '@/lib/http'
+
+const settings = {
+    get() {
+        return http.get('/settings')
+    },
+    save(data: any) {
+        return http.post('/settings', data)
+    }
+}
+
+export default settings

+ 25 - 0
frontend/src/api/template.ts

@@ -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

+ 3 - 6
frontend/src/components/CodeEditor/CodeEditor.vue

@@ -4,16 +4,13 @@ import 'ace-builds/src-noconflict/mode-nginx'
 import 'ace-builds/src-noconflict/theme-monokai'
 import {computed} from 'vue'
 
-const props = defineProps<{
-    content: string
-    defaultHeight?: string
-}>()
+const props = defineProps(['content', 'defaultHeight'])
 
 const emit = defineEmits(['update:content'])
 
 const value = computed({
     get() {
-        return props.content
+        return props.content ?? ''
     },
     set(value) {
         emit('update:content', value)
@@ -27,7 +24,7 @@ const value = computed({
         lang="nginx"
         theme="monokai"
         :style="{
-            minHeight: props.defaultHeight || '100vh'
+            minHeight: defaultHeight || '100vh'
         }"/>
 </template>
 

+ 7 - 4
frontend/src/components/FooterToolbar/FooterToolBar.vue

@@ -26,6 +26,13 @@ export default {
 </script>
 
 <style lang="less" scoped>
+.dark {
+    .ant-pro-footer-toolbar {
+        background: rgba(24, 24, 24, 0.62);
+        border-top: unset;
+    }
+}
+
 .ant-pro-footer-toolbar {
     position: fixed;
     width: 100%;
@@ -36,10 +43,6 @@ export default {
     box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.03);
     background: #ffffff8c;
     border-top: 1px solid #e8e8e8;
-    @media (prefers-color-scheme: dark) {
-        background: rgba(24, 24, 24, 0.62);
-        border-top: unset;
-    }
     padding: 0 24px;
     z-index: 9;
 

+ 8 - 5
frontend/src/components/Logo/Logo.vue

@@ -10,6 +10,14 @@ import logo from '@/assets/img/logo.png'</script>
 </template>
 
 <style lang="less" scoped>
+.dark {
+    .logo {
+        background-color: transparent;
+        -webkit-box-shadow: 1px 1px 0 0 #404040;
+        box-shadow: 1px 1px 0 0 #404040;
+    }
+}
+
 .logo {
     padding: 8px 25px;
     -webkit-box-shadow: 1px 1px 0 0 #e8e8e8;
@@ -20,11 +28,6 @@ import logo from '@/assets/img/logo.png'</script>
     overflow: hidden;
     display: inline-block;
     background-color: #ffffff;
-    @media (prefers-color-scheme: dark) {
-        background-color: transparent;
-        -webkit-box-shadow: 1px 1px 0 0 #404040;
-        box-shadow: 1px 1px 0 0 #404040;
-    }
 
     img {
         height: 46px;

+ 8 - 5
frontend/src/components/PageHeader/PageHeader.vue

@@ -40,18 +40,21 @@ watch(() => route.name, () => {
 </template>
 
 <style lang="less" scoped>
-.page-header {
-    background: #fff;
-    padding: 16px 32px 0;
-    border-bottom: 1px solid #e8e8e8;
-    @media (prefers-color-scheme: dark) {
+.dark {
+    .page-header {
         background: #28292c !important;
         border-bottom: unset;
+
         h1 {
             color: #fafafa;
         }
     }
+}
 
+.page-header {
+    background: #fff;
+    padding: 16px 32px 0;
+    border-bottom: 1px solid #e8e8e8;
 
     .breadcrumb {
         margin-bottom: 16px;

+ 0 - 1
frontend/src/components/SetLanguage/SetLanguage.vue

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

+ 21 - 26
frontend/src/components/StdDataEntry/components/StdSelector.vue

@@ -79,28 +79,28 @@ const _selectedKey = computed({
                 {{ M_value }}
             </div>
             <a-modal
-                    :mask="false"
-                    :visible="visible"
-                    :cancel-text="$gettext('Cancel')"
-                    :ok-text="$gettext('OK')"
-                    :title="$gettext('Selector')"
-                    @cancel="visible=false"
-                    @ok="ok()"
-                    :width="800"
-                    destroyOnClose
+                :mask="false"
+                :visible="visible"
+                :cancel-text="$gettext('Cancel')"
+                :ok-text="$gettext('OK')"
+                :title="$gettext('Selector')"
+                @cancel="visible=false"
+                @ok="ok()"
+                :width="800"
+                destroyOnClose
             >
                 {{ description }}
                 <std-table
-                        :api="api"
-                        :columns="columns"
-                        :data_key="data_key"
-                        :disable_search="disable_search"
-                        :pithy="true"
-                        :get_params="get_params"
-                        :selectionType="selectionType"
-                        :disable_query_params="true"
-                        @onSelected="onSelect"
-                        @onSelectedRecord="onSelectedRecord"
+                    :api="api"
+                    :columns="columns"
+                    :data_key="data_key"
+                    :disable_search="disable_search"
+                    :pithy="true"
+                    :get_params="get_params"
+                    :selectionType="selectionType"
+                    :disable_query_params="true"
+                    @onSelected="onSelect"
+                    @onSelectedRecord="onSelectedRecord"
                 />
             </a-modal>
         </div>
@@ -108,6 +108,7 @@ const _selectedKey = computed({
 </template>
 
 <style lang="less" scoped>
+.dark .std-selector-container
 .std-selector-container {
     height: 39.9px;
     display: flex;
@@ -132,15 +133,9 @@ const _selectedKey = computed({
         cursor: pointer;
         min-width: 180px;
 
-        @media (prefers-color-scheme: dark) {
-            background-color: #1e1f20;
-            border: 1px solid #666666;
-            color: rgba(255, 255, 255, 0.99);
-        }
-
         .value {
 
         }
     }
 }
-</style>
+</style>

+ 0 - 51
frontend/src/components/StdDataEntry/compontents/StdPassword.vue

@@ -1,51 +0,0 @@
-<script setup lang="ts">
-import {computed, ref} from 'vue'
-
-const props = defineProps(['value', 'generate', 'placeholder'])
-const emit = defineEmits(['update:value'])
-
-const M_value = computed({
-    get() {
-        return props.value
-    },
-    set(v) {
-        emit('update:value', v)
-    }
-})
-const visibility = ref(false)
-
-function handle_generate() {
-    visibility.value = true
-    M_value.value = 'xxxx'
-
-    const chars = '0123456789abcdefghijklmnopqrstuvwxyz!@#$%^&*()ABCDEFGHIJKLMNOPQRSTUVWXYZ'
-    const passwordLength = 12
-    let password = ''
-    for (let i = 0; i <= passwordLength; i++) {
-        const randomNumber = Math.floor(Math.random() * chars.length)
-        password += chars.substring(randomNumber, randomNumber + 1)
-    }
-
-    M_value.value = password
-
-}
-</script>
-
-<template>
-    <a-input-group compact>
-        <a-input-password
-                v-if="!visibility"
-                :class="{compact: generate}"
-                v-model:value="M_value" :placeholoder="placeholder"/>
-        <a-input v-else :class="{compact: generate}" v-model:value="M_value" :placeholoder="placeholder"/>
-        <a-button @click="handle_generate" v-if="generate" type="primary">
-            <translate>Generate</translate>
-        </a-button>
-    </a-input-group>
-</template>
-
-<style scoped>
-.compact {
-    width: calc(100% - 91px)
-}
-</style>

+ 0 - 45
frontend/src/components/StdDataEntry/compontents/StdSelect.vue

@@ -1,45 +0,0 @@
-<script setup lang="ts">
-import {computed, ref} from 'vue'
-import {SelectProps} from 'ant-design-vue'
-
-const props = defineProps(['value', 'mask'])
-const emit = defineEmits(['update:value'])
-
-const options = computed(() => {
-    const _options = ref<SelectProps['options']>([])
-
-    for (const [key, value] of Object.entries(props.mask)) {
-        const v = value as any
-        _options.value!.push({label: v?.(), value: key})
-    }
-
-    return _options
-})
-
-const _value = computed({
-    get() {
-        let v
-
-        if (typeof props.mask?.[props.value] === 'function') {
-            v = props.mask[props.value]()
-        } else if (typeof props.mask?.[props.value] === 'string') {
-            v = props.mask[props.value]
-        } else {
-            v = props.value
-        }
-        return v
-    },
-    set(v) {
-        emit('update:value', v)
-    }
-})
-</script>
-
-<template>
-    <a-select v-model:value="_value"
-              :options="options.value" style="min-width: 180px"/>
-</template>
-
-<style lang="less" scoped>
-
-</style>

+ 0 - 137
frontend/src/components/StdDataEntry/compontents/StdSelector.vue

@@ -1,137 +0,0 @@
-<script setup lang="ts">
-import {onMounted, reactive, ref, watch} from 'vue'
-import StdTable from '@/components/StdDataDisplay/StdTable.vue'
-import gettext from '@/gettext'
-
-const {$gettext} = gettext
-const props = defineProps(['selectedKey', 'value', 'recordValueIndex',
-    'selectionType', 'api', 'columns', 'data_key',
-    'disable_search', 'get_params', 'description'])
-const emit = defineEmits(['update:selectedKey', 'changeSelect'])
-const visible = ref(false)
-const M_value = ref('')
-
-onMounted(() => {
-    init()
-})
-
-const selected = ref([])
-
-const record: any = reactive({})
-
-function init() {
-    if (props.selectedKey && !props.value && props.selectionType === 'radio') {
-        props.api.get(props.selectedKey).then((r: any) => {
-            Object.assign(record, r)
-            M_value.value = r[props.recordValueIndex]
-        })
-    }
-}
-
-function show() {
-    visible.value = true
-}
-
-function onSelect(_selected: any) {
-    selected.value = _selected
-}
-
-function onSelectedRecord(r: any) {
-    Object.assign(record, r)
-}
-
-function ok() {
-    visible.value = false
-    if (props.selectionType == 'radio') {
-        emit('update:selectedKey', selected.value[0])
-    } else {
-        emit('update:selectedKey', selected.value)
-    }
-    M_value.value = record[props.recordValueIndex]
-    emit('changeSelect', record)
-}
-
-watch(props, () => {
-    if (!props?.selectedKey) {
-        M_value.value = ''
-    } else if (props.value) {
-        M_value.value = props.value
-    } else {
-        init()
-    }
-})
-</script>
-
-<template>
-    <div class="std-selector-container">
-        <div class="std-selector" @click="show()">
-            <a-input v-model="selectedKey" disabled hidden/>
-            <div class="value">
-                {{ M_value }}
-            </div>
-            <a-modal
-                    :mask="false"
-                    :visible="visible"
-                    :cancel-text="$gettext('Cancel')"
-                    :ok-text="$gettext('OK')"
-                    :title="$gettext('Selector')"
-                    @cancel="visible=false"
-                    @ok="ok()"
-                    :width="800"
-                    destroyOnClose
-            >
-                {{ description }}
-                <std-table
-                        :api="api"
-                        :columns="columns"
-                        :data_key="data_key"
-                        :disable_search="disable_search"
-                        :pithy="true"
-                        :get_params="get_params"
-                        :selectionType="selectionType"
-                        :disable_query_params="true"
-                        @onSelected="onSelect"
-                        @onSelectedRecord="onSelectedRecord"
-                />
-            </a-modal>
-        </div>
-    </div>
-</template>
-
-<style lang="less" scoped>
-.std-selector-container {
-    height: 39.9px;
-    display: flex;
-    align-items: flex-start;
-
-    .std-selector {
-        box-sizing: border-box;
-        font-variant: tabular-nums;
-        list-style: none;
-        font-feature-settings: 'tnum';
-        height: 32px;
-        padding: 4px 11px;
-        color: rgba(0, 0, 0, 0.85);
-        font-size: 14px;
-        line-height: 1.5;
-        background-color: #fff;
-        background-image: none;
-        border: 1px solid #d9d9d9;
-        border-radius: 4px;
-        transition: all 0.3s;
-        margin: 0 10px 0 0;
-        cursor: pointer;
-        min-width: 180px;
-
-        @media (prefers-color-scheme: dark) {
-            background-color: #1e1f20;
-            border: 1px solid #666666;
-            color: rgba(255, 255, 255, 0.99);
-        }
-
-        .value {
-
-        }
-    }
-}
-</style>

+ 356 - 131
frontend/src/language/en/app.po

@@ -9,21 +9,22 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
-#: src/routes/index.ts:116
+#: src/routes/index.ts:134
 msgid "About"
 msgstr "About"
 
-#: src/routes/index.ts:99 src/views/domain/ngx_conf/LogEntry.vue:64
+#: src/routes/index.ts:109 src/views/domain/ngx_conf/LogEntry.vue:64
 msgid "Access Logs"
 msgstr ""
 
-#: src/views/config/Config.vue:24 src/views/domain/DomainList.vue:42
-#: src/views/user/User.vue:43
+#: src/views/cert/Cert.vue:78 src/views/config/config.ts:36
+#: src/views/domain/DomainList.vue:47 src/views/user/User.vue:43
 msgid "Action"
 msgstr "Action"
 
-#: src/components/StdDataDisplay/StdCurd.vue:134
-#: src/components/StdDataDisplay/StdCurd.vue:26
+#: src/components/StdDataDisplay/StdCurd.vue:145
+#: src/components/StdDataDisplay/StdCurd.vue:25
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:26
 msgid "Add"
 msgstr ""
 
@@ -33,47 +34,75 @@ msgstr ""
 msgid "Add Directive Below"
 msgstr "Add Directive Below"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:33
-#: src/views/domain/ngx_conf/LocationEditor.vue:48
+#: src/views/domain/ngx_conf/LocationEditor.vue:45
+#: src/views/domain/ngx_conf/LocationEditor.vue:50
+#: src/views/domain/ngx_conf/LocationEditor.vue:51
+#: src/views/domain/ngx_conf/LocationEditor.vue:60
 msgid "Add Location"
 msgstr "Add Location"
 
-#: src/routes/index.ts:55 src/views/domain/DomainAdd.vue:2
+#: src/routes/index.ts:57 src/views/domain/DomainAdd.vue:2
 msgid "Add Site"
 msgstr "Add Site"
 
-#: src/views/domain/DomainEdit.vue:19
+#: src/views/domain/DomainEdit.vue:18 src/views/domain/DomainEdit.vue:19
 msgid "Advance Mode"
 msgstr "Advance Mode"
 
-#: src/components/StdDataDisplay/StdTable.vue:44
-#: src/views/domain/DomainList.vue:27
+#: src/components/StdDataDisplay/StdTable.vue:54
+#: src/views/domain/DomainList.vue:26
 #, fuzzy
-msgid "Are you sure you want to delete ?"
+msgid "Are you sure you want to delete?"
 msgstr "Are you sure you want to remove this directive?"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:15
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:20
 msgid "Are you sure you want to remove this directive?"
 msgstr "Are you sure you want to remove this directive?"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:9
+#: src/views/domain/ngx_conf/LocationEditor.vue:19
 #, fuzzy
 msgid "Are you sure you want to remove this location?"
 msgstr "Are you sure you want to remove this directive?"
 
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:11
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:12
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:15
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:19
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:20
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:23
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:29
+msgid "Author"
+msgstr ""
+
+#: src/views/preference/Preference.vue:22
+#: src/views/preference/Preference.vue:23
+msgid "Auto"
+msgstr ""
+
+#: src/views/cert/Cert.vue:41
+msgid "Auto Cert"
+msgstr ""
+
+#: src/views/cert/Cert.vue:8
+msgid "Auto cert is enabled, please do not modify this certification."
+msgstr ""
+
 #: src/views/nginx_log/NginxLog.vue:4
 msgid "Auto Refresh"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:78
+#: src/views/domain/cert/IssueCert.vue:71
 msgid "Auto-renewal disabled for %{name}"
 msgstr "Auto-renewal disabled for %{name}"
 
-#: src/views/domain/cert/IssueCert.vue:72
+#: src/views/domain/cert/IssueCert.vue:65
 msgid "Auto-renewal enabled for %{name}"
 msgstr "Auto-renewal enabled for %{name}"
 
-#: src/views/domain/DomainEdit.vue:178 src/views/nginx_log/NginxLog.vue:172
+#: src/views/config/Config.vue:16 src/views/config/Config.vue:17
+#: src/views/config/Config.vue:27 src/views/config/Config.vue:5
+#: src/views/config/ConfigEdit.vue:64 src/views/domain/DomainEdit.vue:187
+#: src/views/nginx_log/NginxLog.vue:173
 msgid "Back"
 msgstr "Back"
 
@@ -86,40 +115,65 @@ msgstr "Back"
 msgid "Base information"
 msgstr "Base information"
 
-#: src/views/domain/DomainEdit.vue:22
+#: src/views/domain/DomainEdit.vue:21 src/views/domain/DomainEdit.vue:22
 msgid "Basic Mode"
 msgstr "Basic Mode"
 
+#: src/components/StdDataDisplay/StdBatchEdit.vue:5
+#: src/components/StdDataDisplay/StdTable.vue:12
+#: src/components/StdDataDisplay/StdTable.vue:13
+#: src/components/StdDataDisplay/StdTable.vue:18
+#, fuzzy
+msgid "Batch Modify"
+msgstr "Modify Config"
+
 #: src/views/other/About.vue:21
 msgid "Build with"
 msgstr "Build with"
 
-#: src/components/StdDataDisplay/StdCurd.vue:28
-#: src/components/StdDataEntry/compontents/StdSelector.vue:11
-#: src/views/config/ConfigEdit.vue:49
+#: src/components/StdDataDisplay/StdBatchEdit.vue:7
+#: src/components/StdDataDisplay/StdCurd.vue:27
+#: src/components/StdDataEntry/components/StdSelector.vue:11
 msgid "Cancel"
 msgstr "Cancel"
 
-#: src/views/domain/cert/CertInfo.vue:24
+#: src/views/domain/cert/CertInfo.vue:19
 msgid "Certificate has expired"
 msgstr "Certificate has expired"
 
-#: src/views/domain/cert/CertInfo.vue:28
+#: src/views/domain/cert/CertInfo.vue:23
 msgid "Certificate is valid"
 msgstr "Certificate is valid"
 
-#: src/views/domain/cert/CertInfo.vue:14
+#: src/views/cert/Cert.vue:12 src/views/domain/cert/Cert.vue:35
 msgid "Certificate Status"
 msgstr "Certificate Status"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:29
-#: src/views/domain/ngx_conf/LocationEditor.vue:21
-#: src/views/domain/ngx_conf/LocationEditor.vue:35
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:175
+#: src/routes/index.ts:87 src/views/cert/Cert.vue:2
+#, fuzzy
+msgid "Certification"
+msgstr "Certificate is valid"
+
+#: src/views/domain/cert/ChangeCert.vue:2
+#: src/views/domain/cert/ChangeCert.vue:3
+#: src/views/domain/cert/ChangeCert.vue:5
+#, fuzzy
+msgid "Change Certificate"
+msgstr "Certificate is valid"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:34
+#: src/views/domain/ngx_conf/LocationEditor.vue:31
+#: src/views/domain/ngx_conf/LocationEditor.vue:47
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:180
 msgid "Comments"
 msgstr "Comments"
 
-#: src/views/domain/DomainAdd.vue:12
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:61
+#, fuzzy
+msgid "Config Templates"
+msgstr "Configurations"
+
+#: src/views/domain/DomainAdd.vue:11
 msgid "Configuration Name"
 msgstr "Configuration Name"
 
@@ -131,8 +185,9 @@ msgstr "Configurations"
 msgid "Configure SSL"
 msgstr "Configure SSL"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:27
-#: src/views/domain/ngx_conf/LocationEditor.vue:41
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:37
+#: src/views/domain/ngx_conf/LocationEditor.vue:37
+#: src/views/domain/ngx_conf/LocationEditor.vue:53
 msgid "Content"
 msgstr "Content"
 
@@ -144,7 +199,7 @@ msgstr "CPU Status"
 msgid "CPU:"
 msgstr "CPU:"
 
-#: src/views/domain/DomainAdd.vue:150
+#: src/views/domain/DomainAdd.vue:149
 msgid "Create Another"
 msgstr "Create Another"
 
@@ -156,7 +211,21 @@ msgstr "Created at"
 msgid "Creating client facilitates communication with the CA server"
 msgstr ""
 
-#: src/routes/index.ts:27
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:22
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:23
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:26
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:32
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:6
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:7
+msgid "Custom"
+msgstr ""
+
+#: src/views/preference/Preference.vue:28
+#: src/views/preference/Preference.vue:29
+msgid "Dark"
+msgstr ""
+
+#: src/routes/index.ts:29
 msgid "Dashboard"
 msgstr "Dashboard"
 
@@ -164,41 +233,58 @@ msgstr "Dashboard"
 msgid "Database (Optional, default: database)"
 msgstr "Database (Optional, default: database)"
 
-#: src/components/StdDataDisplay/StdTable.vue:366
-#: src/views/domain/DomainList.vue:111
+#: src/components/StdDataDisplay/StdTable.vue:527
+#: src/views/domain/DomainList.vue:115
 msgid "Delete"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdTable.vue:120
+#: src/components/StdDataDisplay/StdTable.vue:132
 msgid "Delete ID: %{id}"
 msgstr ""
 
-#: src/views/domain/DomainList.vue:76
+#: src/views/domain/DomainList.vue:81
 msgid "Delete site: %{site_name}"
 msgstr ""
 
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:12
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:13
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:16
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:20
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:21
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:24
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:30
+msgid "Description"
+msgstr ""
+
 #: src/views/other/About.vue:7 src/views/other/About.vue:8
 msgid "Development Mode"
 msgstr "Development Mode"
 
+#: src/views/config/config.ts:20
+msgid "Dir"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:20
 msgid "Directive"
 msgstr "Directive"
 
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:1
 #: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:2
 msgid "Directives"
 msgstr "Directives"
 
-#: src/views/domain/cert/IssueCert.vue:80
+#: src/views/domain/cert/IssueCert.vue:73
 msgid "Disable auto-renewal failed for %{name}"
 msgstr "Disable auto-renewal failed for %{name}"
 
-#: src/views/domain/DomainEdit.vue:10 src/views/domain/DomainList.vue:17
-#: src/views/domain/DomainList.vue:29
+#: src/views/cert/Cert.vue:51 src/views/domain/DomainEdit.vue:10
+#: src/views/domain/DomainEdit.vue:9 src/views/domain/DomainList.vue:16
+#: src/views/domain/DomainList.vue:34 src/views/domain/DomainList.vue:7
+#: src/views/domain/DomainList.vue:8 src/views/domain/DomainList.vue:9
 msgid "Disabled"
 msgstr "Disabled"
 
-#: src/views/domain/DomainEdit.vue:112 src/views/domain/DomainList.vue:64
+#: src/views/domain/DomainEdit.vue:118 src/views/domain/DomainList.vue:69
 msgid "Disabled successfully"
 msgstr "Disabled successfully"
 
@@ -206,19 +292,23 @@ msgstr "Disabled successfully"
 msgid "Disk IO"
 msgstr "Disk IO"
 
-#: src/views/domain/DomainAdd.vue:60
+#: src/views/cert/Cert.vue:32
+msgid "Domain"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:58
 msgid "Domain Config Created Successfully"
 msgstr "Domain Config Created Successfully"
 
-#: src/views/domain/DomainEdit.vue:5
+#: src/views/domain/DomainEdit.vue:4 src/views/domain/DomainEdit.vue:5
 msgid "Edit %{n}"
 msgstr "Edit %{n}"
 
-#: src/routes/index.ts:77 src/views/config/ConfigEdit.vue:2
+#: src/routes/index.ts:79 src/views/config/ConfigEdit.vue:2
 msgid "Edit Configuration"
 msgstr "Edit Configuration"
 
-#: src/routes/index.ts:59
+#: src/routes/index.ts:61
 msgid "Edit Site"
 msgstr "Edit Site"
 
@@ -226,11 +316,11 @@ msgstr "Edit Site"
 msgid "Email (*)"
 msgstr "Email (*)"
 
-#: src/views/domain/cert/IssueCert.vue:74
+#: src/views/domain/cert/IssueCert.vue:67
 msgid "Enable auto-renewal failed for %{name}"
 msgstr "Enable auto-renewal failed for %{name}"
 
-#: src/views/domain/DomainAdd.vue:50
+#: src/views/domain/DomainAdd.vue:51
 msgid "Enable failed"
 msgstr "Enable failed"
 
@@ -238,39 +328,43 @@ msgstr "Enable failed"
 msgid "Enable TLS"
 msgstr "Enable TLS"
 
-#: src/views/domain/DomainEdit.vue:33 src/views/domain/DomainEdit.vue:7
-#: src/views/domain/DomainList.vue:12 src/views/domain/DomainList.vue:20
-#: src/views/domain/DomainList.vue:26
+#: src/views/cert/Cert.vue:48 src/views/domain/DomainEdit.vue:33
+#: src/views/domain/DomainEdit.vue:6 src/views/domain/DomainEdit.vue:7
+#: src/views/domain/DomainList.vue:10 src/views/domain/DomainList.vue:11
+#: src/views/domain/DomainList.vue:12 src/views/domain/DomainList.vue:19
+#: src/views/domain/DomainList.vue:31
 msgid "Enabled"
 msgstr "Enabled"
 
-#: src/views/domain/DomainAdd.vue:46 src/views/domain/DomainEdit.vue:103
-#: src/views/domain/DomainList.vue:54
+#: src/views/domain/DomainAdd.vue:47 src/views/domain/DomainEdit.vue:109
+#: src/views/domain/DomainList.vue:59
 msgid "Enabled successfully"
 msgstr "Enabled successfully"
 
-#: src/views/domain/cert/IssueCert.vue:17
+#: src/views/domain/cert/IssueCert.vue:18
 msgid "Encrypt website with Let's Encrypt"
 msgstr "Encrypt website with Let's Encrypt"
 
-#: src/routes/index.ts:103 src/views/domain/ngx_conf/LogEntry.vue:68
+#: src/routes/index.ts:113 src/views/domain/ngx_conf/LogEntry.vue:68
 msgid "Error Logs"
 msgstr ""
 
-#: src/views/domain/cert/CertInfo.vue:17
+#: src/views/domain/cert/CertInfo.vue:12
 msgid "Expiration Date: %{date}"
 msgstr "Expiration Date: %{date}"
 
 #: src/components/StdDataDisplay/StdTable.vue:12
-#: src/components/StdDataDisplay/StdTable.vue:317
+#: src/components/StdDataDisplay/StdTable.vue:362
+#: src/components/StdDataDisplay/StdTable.vue:6
+#: src/components/StdDataDisplay/StdTable.vue:7
 msgid "Export"
 msgstr ""
 
-#: src/views/domain/DomainEdit.vue:115 src/views/domain/DomainList.vue:68
+#: src/views/domain/DomainEdit.vue:121 src/views/domain/DomainList.vue:73
 msgid "Failed to disable %{msg}"
 msgstr "Failed to disable %{msg}"
 
-#: src/views/domain/DomainEdit.vue:106 src/views/domain/DomainList.vue:58
+#: src/views/domain/DomainEdit.vue:112 src/views/domain/DomainList.vue:63
 msgid "Failed to enable %{msg}"
 msgstr "Failed to enable %{msg}"
 
@@ -278,6 +372,10 @@ msgstr "Failed to enable %{msg}"
 msgid "Failed to get certificate information"
 msgstr ""
 
+#: src/views/config/config.ts:22
+msgid "File"
+msgstr ""
+
 #: src/views/other/Error.vue:3 src/views/other/Error.vue:4
 msgid "File Not Found"
 msgstr "File Not Found"
@@ -290,7 +388,21 @@ msgstr ""
 msgid "Finished"
 msgstr "Finished"
 
-#: src/components/StdDataEntry/compontents/StdPassword.vue:42
+#: src/views/config/ConfigEdit.vue:67
+msgid "Format Code"
+msgstr ""
+
+#: src/views/config/ConfigEdit.vue:52
+#, fuzzy
+msgid "Format error %{msg}"
+msgstr "Save error %{msg}"
+
+#: src/views/config/ConfigEdit.vue:50
+#, fuzzy
+msgid "Format successfully"
+msgstr "Saved successfully"
+
+#: src/components/StdDataEntry/components/StdPassword.vue:42
 msgid "Generate"
 msgstr ""
 
@@ -298,15 +410,23 @@ msgstr ""
 msgid "Generating private key for registering account"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:103
+#: src/views/domain/cert/IssueCert.vue:96
 msgid "Getting the certificate, please wait..."
 msgstr "Getting the certificate, please wait..."
 
-#: src/routes/index.ts:20
+#: src/routes/index.ts:22
 msgid "Home"
 msgstr "Home"
 
-#: src/routes/index.ts:126 src/views/other/Install.vue:128
+#: src/views/preference/Preference.vue:17
+msgid "HTTP Challenge Port"
+msgstr ""
+
+#: src/views/preference/Preference.vue:5
+msgid "HTTP Port"
+msgstr ""
+
+#: src/routes/index.ts:144 src/views/other/Install.vue:128
 msgid "Install"
 msgstr "Install"
 
@@ -315,7 +435,7 @@ msgstr "Install"
 msgid "Install successfully"
 msgstr "Enabled successfully"
 
-#: src/views/domain/cert/CertInfo.vue:15
+#: src/views/domain/cert/CertInfo.vue:10
 msgid "Intermediate Certification Authorities: %{issuer}"
 msgstr "Intermediate Certification Authorities: %{issuer}"
 
@@ -324,23 +444,34 @@ msgstr "Intermediate Certification Authorities: %{issuer}"
 msgid "Issued certificate successfully"
 msgstr "Enabled successfully"
 
+#: src/views/preference/Preference.vue:11
+msgid "Jwt Secret"
+msgstr ""
+
 #: src/views/user/User.vue:26
 msgid "Leave blank for no change"
 msgstr "Leave blank for no change"
 
+#: src/views/preference/Preference.vue:25
+#: src/views/preference/Preference.vue:26
+msgid "Light"
+msgstr ""
+
 #: src/views/dashboard/DashBoard.vue:141
 msgid "Load Averages:"
 msgstr "Load Averages:"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:5
+#: src/views/domain/ngx_conf/LocationEditor.vue:15
+#: src/views/domain/ngx_conf/LocationEditor.vue:8
+#: src/views/domain/ngx_conf/LocationEditor.vue:9
 msgid "Location"
 msgstr "Location"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:39
+#: src/views/domain/ngx_conf/LocationEditor.vue:40
 msgid "Locations"
 msgstr "Locations"
 
-#: src/routes/index.ts:132 src/views/other/Login.vue:103
+#: src/routes/index.ts:150 src/views/other/Login.vue:103
 msgid "Login"
 msgstr "Login"
 
@@ -352,7 +483,7 @@ msgstr "Login successful"
 msgid "Logout successful"
 msgstr "Logout successful"
 
-#: src/views/domain/cert/IssueCert.vue:226
+#: src/views/domain/cert/IssueCert.vue:211
 msgid ""
 "Make sure you have configured a reverse proxy for .well-known directory to "
 "HTTPChallengePort (default: 9180) before getting the certificate."
@@ -360,15 +491,15 @@ msgstr ""
 "Make sure you have configured a reverse proxy for .well-known directory to "
 "HTTPChallengePort (default: 9180) before getting the certificate."
 
-#: src/routes/index.ts:68
+#: src/routes/index.ts:70
 msgid "Manage Configs"
 msgstr "Manage Configs"
 
-#: src/routes/index.ts:43 src/views/domain/DomainList.vue:2
+#: src/routes/index.ts:45 src/views/domain/DomainList.vue:2
 msgid "Manage Sites"
 msgstr "Manage Sites"
 
-#: src/routes/index.ts:35 src/views/user/User.vue:2
+#: src/routes/index.ts:37 src/views/user/User.vue:2
 msgid "Manage Users"
 msgstr "Manage Users"
 
@@ -380,21 +511,28 @@ msgstr "Memory"
 msgid "Memory and Storage"
 msgstr "Memory and Storage"
 
-#: src/components/StdDataDisplay/StdCurd.vue:26
-#: src/components/StdDataDisplay/StdTable.vue:18
-#: src/components/StdDataDisplay/StdTable.vue:19
-#: src/components/StdDataDisplay/StdTable.vue:24
-#: src/components/StdDataDisplay/StdTable.vue:34
-#: src/components/StdDataDisplay/StdTable.vue:36
+#: src/components/StdDataDisplay/StdCurd.vue:25
+#: src/components/StdDataDisplay/StdTable.vue:25
+#: src/components/StdDataDisplay/StdTable.vue:26
+#: src/components/StdDataDisplay/StdTable.vue:31
+#: src/components/StdDataDisplay/StdTable.vue:44
+#: src/components/StdDataDisplay/StdTable.vue:46
 #, fuzzy
 msgid "Modify"
 msgstr "Modify Config"
 
-#: src/views/domain/DomainAdd.vue:147
+#: src/views/domain/DomainAdd.vue:146
 msgid "Modify Config"
 msgstr "Modify Config"
 
-#: src/views/config/Config.vue:12 src/views/domain/DomainList.vue:14
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:10
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:9
+#, fuzzy
+msgid "Multi-line Directive"
+msgstr "Single Directive"
+
+#: src/views/cert/Cert.vue:16 src/views/config/config.ts:9
+#: src/views/domain/DomainEdit.vue:36 src/views/domain/DomainList.vue:15
 msgid "Name"
 msgstr "Name"
 
@@ -414,45 +552,50 @@ msgstr "Network Total Receive"
 msgid "Network Total Send"
 msgstr "Network Total Send"
 
-#: src/views/domain/DomainAdd.vue:137
+#: src/views/domain/DomainAdd.vue:136
 msgid "Next"
 msgstr "Next"
 
-#: src/routes/index.ts:93 src/views/nginx_log/NginxLog.vue:2
+#: src/views/preference/Preference.vue:33
+msgid "Nginx Access Log Path"
+msgstr ""
+
+#: src/views/preference/Preference.vue:36
+msgid "Nginx Error Log Path"
+msgstr ""
+
+#: src/routes/index.ts:103 src/views/nginx_log/NginxLog.vue:2
 msgid "Nginx Log"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdTable.vue:42
-#: src/views/domain/DomainList.vue:25
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:17
-#: src/views/domain/ngx_conf/LocationEditor.vue:11
+#: src/components/StdDataDisplay/StdTable.vue:52
+#: src/views/domain/DomainList.vue:24
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:22
+#: src/views/domain/ngx_conf/LocationEditor.vue:21
 msgid "No"
 msgstr "No"
 
-#: src/routes/index.ts:138 src/routes/index.ts:140
+#: src/routes/index.ts:156 src/routes/index.ts:158
 msgid "Not Found"
 msgstr "Not Found"
 
-#: src/views/domain/cert/CertInfo.vue:19
+#: src/views/domain/cert/CertInfo.vue:14
 msgid "Not Valid Before: %{date}"
 msgstr "Not Valid Before: %{date}"
 
-#: src/views/domain/cert/IssueCert.vue:218
-msgid ""
-"Note: The server_name in the current configuration must be the domain name "
-"you need to get the certificate."
+#: src/views/domain/cert/IssueCert.vue:38
+msgid "Note"
 msgstr ""
-"Note: The server_name in the current configuration must be the domain name "
-"you need to get the certificate."
 
 #: src/language/constants.ts:16 src/views/domain/cert/IssueCert.vue:3
 msgid "Obtaining certificate"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdCurd.vue:29
-#: src/components/StdDataDisplay/StdTable.vue:43
-#: src/components/StdDataEntry/compontents/StdSelector.vue:12
-#: src/views/domain/DomainList.vue:26
+#: src/components/StdDataDisplay/StdBatchEdit.vue:8
+#: src/components/StdDataDisplay/StdCurd.vue:28
+#: src/components/StdDataDisplay/StdTable.vue:53
+#: src/components/StdDataEntry/components/StdSelector.vue:12
+#: src/views/domain/DomainList.vue:25
 msgid "OK"
 msgstr ""
 
@@ -472,8 +615,8 @@ msgstr "Password"
 msgid "Password (*)"
 msgstr "Password (*)"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:24
-#: src/views/domain/ngx_conf/LocationEditor.vue:38
+#: src/views/domain/ngx_conf/LocationEditor.vue:34
+#: src/views/domain/ngx_conf/LocationEditor.vue:50
 msgid "Path"
 msgstr "Path"
 
@@ -489,6 +632,10 @@ msgstr "Please input your password!"
 msgid "Please input your username!"
 msgstr "Please input your username!"
 
+#: src/routes/index.ts:126 src/views/preference/Preference.vue:2
+msgid "Preference"
+msgstr ""
+
 #: src/language/constants.ts:12
 #, fuzzy
 msgid "Preparing lego configurations"
@@ -522,35 +669,56 @@ msgstr ""
 msgid "Reloading nginx"
 msgstr ""
 
+#: src/components/StdDataDisplay/StdTable.vue:10
 #: src/components/StdDataDisplay/StdTable.vue:15
+#: src/components/StdDataDisplay/StdTable.vue:9
 msgid "Reset"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:52 src/views/domain/DomainEdit.vue:181
+#: src/views/preference/Preference.vue:8
+#, fuzzy
+msgid "Run Mode"
+msgstr "Advance Mode"
+
+#: src/views/config/ConfigEdit.vue:70 src/views/domain/DomainEdit.vue:190
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:33
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:34
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:40
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
+#: src/views/preference/Preference.vue:43
+#: src/views/preference/Preference.vue:44
 msgid "Save"
 msgstr "Save"
 
-#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:32
-#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:33
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:34
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:35
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:36
 msgid "Save Directive"
 msgstr "Save Directive"
 
-#: src/views/config/ConfigEdit.vue:36 src/views/domain/DomainAdd.vue:54
+#: src/views/config/ConfigEdit.vue:43 src/views/domain/DomainAdd.vue:55
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:37
 msgid "Save error %{msg}"
 msgstr "Save error %{msg}"
 
-#: src/components/StdDataDisplay/StdCurd.vue:102
+#: src/components/StdDataDisplay/StdBatchEdit.vue:40
+#: src/views/preference/Preference.vue:39
+#, fuzzy
+msgid "Save successfully"
+msgstr "Saved successfully"
+
+#: src/components/StdDataDisplay/StdCurd.vue:108
 #, fuzzy
 msgid "Save Successfully"
 msgstr "Saved successfully"
 
-#: src/views/config/ConfigEdit.vue:34 src/views/domain/DomainAdd.vue:43
-#: src/views/domain/DomainEdit.vue:91
+#: src/views/config/ConfigEdit.vue:41 src/views/domain/DomainAdd.vue:44
+#: src/views/domain/DomainEdit.vue:97
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:35
 msgid "Saved successfully"
 msgstr "Saved successfully"
 
-#: src/components/StdDataEntry/compontents/StdSelector.vue:13
+#: src/components/StdDataEntry/components/StdSelector.vue:13
 msgid "Selector"
 msgstr ""
 
@@ -558,12 +726,14 @@ msgstr ""
 msgid "Send"
 msgstr "Send"
 
-#: src/components/StdDataDisplay/StdTable.vue:140
-#: src/components/StdDataDisplay/StdTable.vue:298
-#: src/views/config/ConfigEdit.vue:22 src/views/domain/DomainEdit.vue:56
-#: src/views/domain/DomainEdit.vue:68 src/views/domain/DomainEdit.vue:77
-#: src/views/domain/DomainEdit.vue:94 src/views/domain/DomainList.vue:78
-#: src/views/other/Install.vue:71
+#: src/components/StdDataDisplay/StdBatchEdit.vue:43
+#: src/components/StdDataDisplay/StdTable.vue:168
+#: src/components/StdDataDisplay/StdTable.vue:343
+#: src/components/StdDataDisplay/StdTable.vue:463
+#: src/views/config/ConfigEdit.vue:29 src/views/domain/DomainEdit.vue:100
+#: src/views/domain/DomainEdit.vue:62 src/views/domain/DomainEdit.vue:74
+#: src/views/domain/DomainEdit.vue:83 src/views/domain/DomainList.vue:83
+#: src/views/other/Install.vue:71 src/views/preference/Preference.vue:41
 msgid "Server error"
 msgstr "Server error"
 
@@ -571,33 +741,49 @@ msgstr "Server error"
 msgid "Server Info"
 msgstr "Server Info"
 
-#: src/views/domain/cert/IssueCert.vue:29
+#: src/views/domain/cert/IssueCert.vue:30
 msgid "server_name not found in directives"
 msgstr "server_name not found in directives"
 
-#: src/views/domain/cert/IssueCert.vue:209 src/views/domain/DomainAdd.vue:112
+#: src/views/domain/cert/IssueCert.vue:195 src/views/domain/DomainAdd.vue:111
 msgid "server_name parameter is required"
 msgstr "server_name parameter is required"
 
-#: src/views/domain/cert/IssueCert.vue:212
-#: src/views/domain/cert/IssueCert.vue:35
-msgid "server_name parameters more than one"
-msgstr "server_name parameters more than one"
-
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:6
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:7
 msgid "Single Directive"
 msgstr "Single Directive"
 
-#: src/routes/index.ts:107
+#: src/routes/index.ts:117
 #, fuzzy
 msgid "Site Logs"
 msgstr "Sites List"
 
-#: src/routes/index.ts:51
+#: src/routes/index.ts:53
 msgid "Sites List"
 msgstr "Sites List"
 
-#: src/views/domain/DomainList.vue:19
+#: src/views/cert/Cert.vue:65
+#, fuzzy
+msgid "SSL Certificate Key Path"
+msgstr "Certificate Status"
+
+#: src/views/cert/Cert.vue:58
+#, fuzzy
+msgid "SSL Certificate Path"
+msgstr "Certificate Status"
+
+#: src/views/cert/Cert.vue:19
+#, fuzzy
+msgid "SSL Certification Content"
+msgstr "Certificate Status"
+
+#: src/views/cert/Cert.vue:22
+#, fuzzy
+msgid "SSL Certification Key Content"
+msgstr "Certificate Status"
+
+#: src/views/domain/DomainList.vue:24
 msgid "Status"
 msgstr "Status"
 
@@ -605,7 +791,7 @@ msgstr "Status"
 msgid "Storage"
 msgstr "Storage"
 
-#: src/views/domain/cert/CertInfo.vue:16
+#: src/views/domain/cert/CertInfo.vue:11
 msgid "Subject Name: %{name}"
 msgstr "Subject Name: %{name}"
 
@@ -618,11 +804,15 @@ msgstr "Swap"
 msgid "Table"
 msgstr "Enabled"
 
-#: src/routes/index.ts:85 src/views/pty/Terminal.vue:2
+#: src/routes/index.ts:95 src/views/pty/Terminal.vue:2
 msgid "Terminal"
 msgstr "Terminal"
 
-#: src/views/domain/cert/IssueCert.vue:222
+#: src/views/preference/Preference.vue:14
+msgid "Terminal Start Command"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:207
 msgid ""
 "The certificate for the domain will be checked every hour, and will be "
 "renewed if it has been more than 1 month since it was last issued."
@@ -634,15 +824,37 @@ msgstr ""
 msgid "The filename cannot contain the following characters: %{c}"
 msgstr "The filename cannot contain the following characters: %{c}"
 
+#: src/views/domain/cert/IssueCert.vue:203
+#, fuzzy
+msgid ""
+"The server_name in the current configuration must be the domain name you "
+"need to get the certificate."
+msgstr ""
+"Note: The server_name in the current configuration must be the domain name "
+"you need to get the certificate."
+
 #: src/language/constants.ts:6
 msgid "The username or password is incorrect"
 msgstr ""
 
-#: src/views/config/Config.vue:17 src/views/domain/DomainList.vue:36
-#: src/views/user/User.vue:37
+#: src/views/preference/Preference.vue:20
+msgid "Theme"
+msgstr ""
+
+#: src/views/config/config.ts:14
+msgid "Type"
+msgstr ""
+
+#: src/views/cert/Cert.vue:72 src/views/config/config.ts:29
+#: src/views/domain/DomainList.vue:41 src/views/user/User.vue:37
 msgid "Updated at"
 msgstr "Updated at"
 
+#: src/components/StdDataDisplay/StdTable.vue:461
+#, fuzzy
+msgid "Updated successfully"
+msgstr "Saved successfully"
+
 #: src/views/dashboard/DashBoard.vue:137
 msgid "Uptime:"
 msgstr "Uptime:"
@@ -659,7 +871,13 @@ msgstr "Username (*)"
 msgid "Using HTTP01 challenge provider"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:26 src/views/domain/DomainAdd.vue:24
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:10
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:13
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:9
+msgid "View"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:27 src/views/domain/DomainAdd.vue:22
 msgid "Warning"
 msgstr "Warning"
 
@@ -676,8 +894,8 @@ msgstr ""
 msgid "Writing certificate to disk"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:16
-#: src/views/domain/ngx_conf/LocationEditor.vue:10
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:21
+#: src/views/domain/ngx_conf/LocationEditor.vue:20
 msgid "Yes"
 msgstr "Yes"
 
@@ -686,6 +904,13 @@ msgctxt "Project"
 msgid "License"
 msgstr "License"
 
+#, fuzzy
+#~ msgid "Are you sure you want to delete ?"
+#~ msgstr "Are you sure you want to remove this directive?"
+
+#~ msgid "server_name parameters more than one"
+#~ msgstr "server_name parameters more than one"
+
 #~ msgid "404 Not Found"
 #~ msgstr "404 Not Found"
 

+ 357 - 137
frontend/src/language/messages.pot

@@ -2,23 +2,25 @@ msgid ""
 msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 
-#: src/routes/index.ts:116
+#: src/routes/index.ts:134
 msgid "About"
 msgstr ""
 
-#: src/routes/index.ts:99
+#: src/routes/index.ts:109
 #: src/views/domain/ngx_conf/LogEntry.vue:64
 msgid "Access Logs"
 msgstr ""
 
-#: src/views/config/Config.vue:24
-#: src/views/domain/DomainList.vue:42
+#: src/views/cert/Cert.vue:78
+#: src/views/config/config.ts:36
+#: src/views/domain/DomainList.vue:47
 #: src/views/user/User.vue:43
 msgid "Action"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdCurd.vue:134
-#: src/components/StdDataDisplay/StdCurd.vue:26
+#: src/components/StdDataDisplay/StdCurd.vue:145
+#: src/components/StdDataDisplay/StdCurd.vue:25
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:26
 msgid "Add"
 msgstr ""
 
@@ -28,47 +30,78 @@ msgstr ""
 msgid "Add Directive Below"
 msgstr ""
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:33
-#: src/views/domain/ngx_conf/LocationEditor.vue:48
+#: src/views/domain/ngx_conf/LocationEditor.vue:45
+#: src/views/domain/ngx_conf/LocationEditor.vue:50
+#: src/views/domain/ngx_conf/LocationEditor.vue:51
+#: src/views/domain/ngx_conf/LocationEditor.vue:60
 msgid "Add Location"
 msgstr ""
 
-#: src/routes/index.ts:55
+#: src/routes/index.ts:57
 #: src/views/domain/DomainAdd.vue:2
 msgid "Add Site"
 msgstr ""
 
+#: src/views/domain/DomainEdit.vue:18
 #: src/views/domain/DomainEdit.vue:19
 msgid "Advance Mode"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdTable.vue:44
-#: src/views/domain/DomainList.vue:27
-msgid "Are you sure you want to delete ?"
+#: src/components/StdDataDisplay/StdTable.vue:54
+#: src/views/domain/DomainList.vue:26
+msgid "Are you sure you want to delete?"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:15
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:20
 msgid "Are you sure you want to remove this directive?"
 msgstr ""
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:9
+#: src/views/domain/ngx_conf/LocationEditor.vue:19
 msgid "Are you sure you want to remove this location?"
 msgstr ""
 
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:11
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:12
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:15
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:19
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:20
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:23
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:29
+msgid "Author"
+msgstr ""
+
+#: src/views/preference/Preference.vue:22
+#: src/views/preference/Preference.vue:23
+msgid "Auto"
+msgstr ""
+
+#: src/views/cert/Cert.vue:41
+msgid "Auto Cert"
+msgstr ""
+
+#: src/views/cert/Cert.vue:8
+msgid "Auto cert is enabled, please do not modify this certification."
+msgstr ""
+
 #: src/views/nginx_log/NginxLog.vue:4
 msgid "Auto Refresh"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:78
+#: src/views/domain/cert/IssueCert.vue:71
 msgid "Auto-renewal disabled for %{name}"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:72
+#: src/views/domain/cert/IssueCert.vue:65
 msgid "Auto-renewal enabled for %{name}"
 msgstr ""
 
-#: src/views/domain/DomainEdit.vue:178
-#: src/views/nginx_log/NginxLog.vue:172
+#: src/views/config/Config.vue:16
+#: src/views/config/Config.vue:17
+#: src/views/config/Config.vue:27
+#: src/views/config/Config.vue:5
+#: src/views/config/ConfigEdit.vue:64
+#: src/views/domain/DomainEdit.vue:187
+#: src/views/nginx_log/NginxLog.vue:173
 msgid "Back"
 msgstr ""
 
@@ -80,40 +113,64 @@ msgstr ""
 msgid "Base information"
 msgstr ""
 
+#: src/views/domain/DomainEdit.vue:21
 #: src/views/domain/DomainEdit.vue:22
 msgid "Basic Mode"
 msgstr ""
 
+#: src/components/StdDataDisplay/StdBatchEdit.vue:5
+#: src/components/StdDataDisplay/StdTable.vue:12
+#: src/components/StdDataDisplay/StdTable.vue:13
+#: src/components/StdDataDisplay/StdTable.vue:18
+msgid "Batch Modify"
+msgstr ""
+
 #: src/views/other/About.vue:21
 msgid "Build with"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdCurd.vue:28
-#: src/components/StdDataEntry/compontents/StdSelector.vue:11
-#: src/views/config/ConfigEdit.vue:49
+#: src/components/StdDataDisplay/StdBatchEdit.vue:7
+#: src/components/StdDataDisplay/StdCurd.vue:27
+#: src/components/StdDataEntry/components/StdSelector.vue:11
 msgid "Cancel"
 msgstr ""
 
-#: src/views/domain/cert/CertInfo.vue:24
+#: src/views/domain/cert/CertInfo.vue:19
 msgid "Certificate has expired"
 msgstr ""
 
-#: src/views/domain/cert/CertInfo.vue:28
+#: src/views/domain/cert/CertInfo.vue:23
 msgid "Certificate is valid"
 msgstr ""
 
-#: src/views/domain/cert/CertInfo.vue:14
+#: src/views/cert/Cert.vue:12
+#: src/views/domain/cert/Cert.vue:35
 msgid "Certificate Status"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:29
-#: src/views/domain/ngx_conf/LocationEditor.vue:21
-#: src/views/domain/ngx_conf/LocationEditor.vue:35
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:175
+#: src/routes/index.ts:87
+#: src/views/cert/Cert.vue:2
+msgid "Certification"
+msgstr ""
+
+#: src/views/domain/cert/ChangeCert.vue:2
+#: src/views/domain/cert/ChangeCert.vue:3
+#: src/views/domain/cert/ChangeCert.vue:5
+msgid "Change Certificate"
+msgstr ""
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:34
+#: src/views/domain/ngx_conf/LocationEditor.vue:31
+#: src/views/domain/ngx_conf/LocationEditor.vue:47
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:180
 msgid "Comments"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:12
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:61
+msgid "Config Templates"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:11
 msgid "Configuration Name"
 msgstr ""
 
@@ -125,8 +182,9 @@ msgstr ""
 msgid "Configure SSL"
 msgstr ""
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:27
-#: src/views/domain/ngx_conf/LocationEditor.vue:41
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:37
+#: src/views/domain/ngx_conf/LocationEditor.vue:37
+#: src/views/domain/ngx_conf/LocationEditor.vue:53
 msgid "Content"
 msgstr ""
 
@@ -138,7 +196,7 @@ msgstr ""
 msgid "CPU:"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:150
+#: src/views/domain/DomainAdd.vue:149
 msgid "Create Another"
 msgstr ""
 
@@ -150,7 +208,21 @@ msgstr ""
 msgid "Creating client facilitates communication with the CA server"
 msgstr ""
 
-#: src/routes/index.ts:27
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:22
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:23
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:26
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:32
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:6
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:7
+msgid "Custom"
+msgstr ""
+
+#: src/views/preference/Preference.vue:28
+#: src/views/preference/Preference.vue:29
+msgid "Dark"
+msgstr ""
+
+#: src/routes/index.ts:29
 msgid "Dashboard"
 msgstr ""
 
@@ -158,44 +230,64 @@ msgstr ""
 msgid "Database (Optional, default: database)"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdTable.vue:366
-#: src/views/domain/DomainList.vue:111
+#: src/components/StdDataDisplay/StdTable.vue:527
+#: src/views/domain/DomainList.vue:115
 msgid "Delete"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdTable.vue:120
+#: src/components/StdDataDisplay/StdTable.vue:132
 msgid "Delete ID: %{id}"
 msgstr ""
 
-#: src/views/domain/DomainList.vue:76
+#: src/views/domain/DomainList.vue:81
 msgid "Delete site: %{site_name}"
 msgstr ""
 
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:12
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:13
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:16
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:20
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:21
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:24
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:30
+msgid "Description"
+msgstr ""
+
 #: src/views/other/About.vue:7
 #: src/views/other/About.vue:8
 msgid "Development Mode"
 msgstr ""
 
+#: src/views/config/config.ts:20
+msgid "Dir"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:20
 msgid "Directive"
 msgstr ""
 
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:1
 #: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:2
 msgid "Directives"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:80
+#: src/views/domain/cert/IssueCert.vue:73
 msgid "Disable auto-renewal failed for %{name}"
 msgstr ""
 
+#: src/views/cert/Cert.vue:51
 #: src/views/domain/DomainEdit.vue:10
-#: src/views/domain/DomainList.vue:17
-#: src/views/domain/DomainList.vue:29
+#: src/views/domain/DomainEdit.vue:9
+#: src/views/domain/DomainList.vue:16
+#: src/views/domain/DomainList.vue:34
+#: src/views/domain/DomainList.vue:7
+#: src/views/domain/DomainList.vue:8
+#: src/views/domain/DomainList.vue:9
 msgid "Disabled"
 msgstr ""
 
-#: src/views/domain/DomainEdit.vue:112
-#: src/views/domain/DomainList.vue:64
+#: src/views/domain/DomainEdit.vue:118
+#: src/views/domain/DomainList.vue:69
 msgid "Disabled successfully"
 msgstr ""
 
@@ -203,20 +295,25 @@ msgstr ""
 msgid "Disk IO"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:60
+#: src/views/cert/Cert.vue:32
+msgid "Domain"
+msgstr ""
+
+#: src/views/domain/DomainAdd.vue:58
 msgid "Domain Config Created Successfully"
 msgstr ""
 
+#: src/views/domain/DomainEdit.vue:4
 #: src/views/domain/DomainEdit.vue:5
 msgid "Edit %{n}"
 msgstr ""
 
-#: src/routes/index.ts:77
+#: src/routes/index.ts:79
 #: src/views/config/ConfigEdit.vue:2
 msgid "Edit Configuration"
 msgstr ""
 
-#: src/routes/index.ts:59
+#: src/routes/index.ts:61
 msgid "Edit Site"
 msgstr ""
 
@@ -224,11 +321,11 @@ msgstr ""
 msgid "Email (*)"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:74
+#: src/views/domain/cert/IssueCert.vue:67
 msgid "Enable auto-renewal failed for %{name}"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:50
+#: src/views/domain/DomainAdd.vue:51
 msgid "Enable failed"
 msgstr ""
 
@@ -236,45 +333,51 @@ msgstr ""
 msgid "Enable TLS"
 msgstr ""
 
+#: src/views/cert/Cert.vue:48
 #: src/views/domain/DomainEdit.vue:33
+#: src/views/domain/DomainEdit.vue:6
 #: src/views/domain/DomainEdit.vue:7
+#: src/views/domain/DomainList.vue:10
+#: src/views/domain/DomainList.vue:11
 #: src/views/domain/DomainList.vue:12
-#: src/views/domain/DomainList.vue:20
-#: src/views/domain/DomainList.vue:26
+#: src/views/domain/DomainList.vue:19
+#: src/views/domain/DomainList.vue:31
 msgid "Enabled"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:46
-#: src/views/domain/DomainEdit.vue:103
-#: src/views/domain/DomainList.vue:54
+#: src/views/domain/DomainAdd.vue:47
+#: src/views/domain/DomainEdit.vue:109
+#: src/views/domain/DomainList.vue:59
 msgid "Enabled successfully"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:17
+#: src/views/domain/cert/IssueCert.vue:18
 msgid "Encrypt website with Let's Encrypt"
 msgstr ""
 
-#: src/routes/index.ts:103
+#: src/routes/index.ts:113
 #: src/views/domain/ngx_conf/LogEntry.vue:68
 msgid "Error Logs"
 msgstr ""
 
-#: src/views/domain/cert/CertInfo.vue:17
+#: src/views/domain/cert/CertInfo.vue:12
 msgid "Expiration Date: %{date}"
 msgstr ""
 
 #: src/components/StdDataDisplay/StdTable.vue:12
-#: src/components/StdDataDisplay/StdTable.vue:317
+#: src/components/StdDataDisplay/StdTable.vue:362
+#: src/components/StdDataDisplay/StdTable.vue:6
+#: src/components/StdDataDisplay/StdTable.vue:7
 msgid "Export"
 msgstr ""
 
-#: src/views/domain/DomainEdit.vue:115
-#: src/views/domain/DomainList.vue:68
+#: src/views/domain/DomainEdit.vue:121
+#: src/views/domain/DomainList.vue:73
 msgid "Failed to disable %{msg}"
 msgstr ""
 
-#: src/views/domain/DomainEdit.vue:106
-#: src/views/domain/DomainList.vue:58
+#: src/views/domain/DomainEdit.vue:112
+#: src/views/domain/DomainList.vue:63
 msgid "Failed to enable %{msg}"
 msgstr ""
 
@@ -282,6 +385,10 @@ msgstr ""
 msgid "Failed to get certificate information"
 msgstr ""
 
+#: src/views/config/config.ts:22
+msgid "File"
+msgstr ""
+
 #: src/views/other/Error.vue:3
 #: src/views/other/Error.vue:4
 msgid "File Not Found"
@@ -296,7 +403,19 @@ msgstr ""
 msgid "Finished"
 msgstr ""
 
-#: src/components/StdDataEntry/compontents/StdPassword.vue:42
+#: src/views/config/ConfigEdit.vue:67
+msgid "Format Code"
+msgstr ""
+
+#: src/views/config/ConfigEdit.vue:52
+msgid "Format error %{msg}"
+msgstr ""
+
+#: src/views/config/ConfigEdit.vue:50
+msgid "Format successfully"
+msgstr ""
+
+#: src/components/StdDataEntry/components/StdPassword.vue:42
 msgid "Generate"
 msgstr ""
 
@@ -304,15 +423,23 @@ msgstr ""
 msgid "Generating private key for registering account"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:103
+#: src/views/domain/cert/IssueCert.vue:96
 msgid "Getting the certificate, please wait..."
 msgstr ""
 
-#: src/routes/index.ts:20
+#: src/routes/index.ts:22
 msgid "Home"
 msgstr ""
 
-#: src/routes/index.ts:126
+#: src/views/preference/Preference.vue:17
+msgid "HTTP Challenge Port"
+msgstr ""
+
+#: src/views/preference/Preference.vue:5
+msgid "HTTP Port"
+msgstr ""
+
+#: src/routes/index.ts:144
 #: src/views/other/Install.vue:128
 msgid "Install"
 msgstr ""
@@ -321,7 +448,7 @@ msgstr ""
 msgid "Install successfully"
 msgstr ""
 
-#: src/views/domain/cert/CertInfo.vue:15
+#: src/views/domain/cert/CertInfo.vue:10
 msgid "Intermediate Certification Authorities: %{issuer}"
 msgstr ""
 
@@ -329,23 +456,34 @@ msgstr ""
 msgid "Issued certificate successfully"
 msgstr ""
 
+#: src/views/preference/Preference.vue:11
+msgid "Jwt Secret"
+msgstr ""
+
 #: src/views/user/User.vue:26
 msgid "Leave blank for no change"
 msgstr ""
 
+#: src/views/preference/Preference.vue:25
+#: src/views/preference/Preference.vue:26
+msgid "Light"
+msgstr ""
+
 #: src/views/dashboard/DashBoard.vue:141
 msgid "Load Averages:"
 msgstr ""
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:5
+#: src/views/domain/ngx_conf/LocationEditor.vue:15
+#: src/views/domain/ngx_conf/LocationEditor.vue:8
+#: src/views/domain/ngx_conf/LocationEditor.vue:9
 msgid "Location"
 msgstr ""
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:39
+#: src/views/domain/ngx_conf/LocationEditor.vue:40
 msgid "Locations"
 msgstr ""
 
-#: src/routes/index.ts:132
+#: src/routes/index.ts:150
 #: src/views/other/Login.vue:103
 msgid "Login"
 msgstr ""
@@ -358,20 +496,20 @@ msgstr ""
 msgid "Logout successful"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:226
+#: src/views/domain/cert/IssueCert.vue:211
 msgid "Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate."
 msgstr ""
 
-#: src/routes/index.ts:68
+#: src/routes/index.ts:70
 msgid "Manage Configs"
 msgstr ""
 
-#: src/routes/index.ts:43
+#: src/routes/index.ts:45
 #: src/views/domain/DomainList.vue:2
 msgid "Manage Sites"
 msgstr ""
 
-#: src/routes/index.ts:35
+#: src/routes/index.ts:37
 #: src/views/user/User.vue:2
 msgid "Manage Users"
 msgstr ""
@@ -384,21 +522,28 @@ msgstr ""
 msgid "Memory and Storage"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdCurd.vue:26
-#: src/components/StdDataDisplay/StdTable.vue:18
-#: src/components/StdDataDisplay/StdTable.vue:19
-#: src/components/StdDataDisplay/StdTable.vue:24
-#: src/components/StdDataDisplay/StdTable.vue:34
-#: src/components/StdDataDisplay/StdTable.vue:36
+#: src/components/StdDataDisplay/StdCurd.vue:25
+#: src/components/StdDataDisplay/StdTable.vue:25
+#: src/components/StdDataDisplay/StdTable.vue:26
+#: src/components/StdDataDisplay/StdTable.vue:31
+#: src/components/StdDataDisplay/StdTable.vue:44
+#: src/components/StdDataDisplay/StdTable.vue:46
 msgid "Modify"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:147
+#: src/views/domain/DomainAdd.vue:146
 msgid "Modify Config"
 msgstr ""
 
-#: src/views/config/Config.vue:12
-#: src/views/domain/DomainList.vue:14
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:10
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:9
+msgid "Multi-line Directive"
+msgstr ""
+
+#: src/views/cert/Cert.vue:16
+#: src/views/config/config.ts:9
+#: src/views/domain/DomainEdit.vue:36
+#: src/views/domain/DomainList.vue:15
 msgid "Name"
 msgstr ""
 
@@ -418,33 +563,41 @@ msgstr ""
 msgid "Network Total Send"
 msgstr ""
 
-#: src/views/domain/DomainAdd.vue:137
+#: src/views/domain/DomainAdd.vue:136
 msgid "Next"
 msgstr ""
 
-#: src/routes/index.ts:93
+#: src/views/preference/Preference.vue:33
+msgid "Nginx Access Log Path"
+msgstr ""
+
+#: src/views/preference/Preference.vue:36
+msgid "Nginx Error Log Path"
+msgstr ""
+
+#: src/routes/index.ts:103
 #: src/views/nginx_log/NginxLog.vue:2
 msgid "Nginx Log"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdTable.vue:42
-#: src/views/domain/DomainList.vue:25
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:17
-#: src/views/domain/ngx_conf/LocationEditor.vue:11
+#: src/components/StdDataDisplay/StdTable.vue:52
+#: src/views/domain/DomainList.vue:24
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:22
+#: src/views/domain/ngx_conf/LocationEditor.vue:21
 msgid "No"
 msgstr ""
 
-#: src/routes/index.ts:138
-#: src/routes/index.ts:140
+#: src/routes/index.ts:156
+#: src/routes/index.ts:158
 msgid "Not Found"
 msgstr ""
 
-#: src/views/domain/cert/CertInfo.vue:19
+#: src/views/domain/cert/CertInfo.vue:14
 msgid "Not Valid Before: %{date}"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:218
-msgid "Note: The server_name in the current configuration must be the domain name you need to get the certificate."
+#: src/views/domain/cert/IssueCert.vue:38
+msgid "Note"
 msgstr ""
 
 #: src/language/constants.ts:16
@@ -452,10 +605,11 @@ msgstr ""
 msgid "Obtaining certificate"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdCurd.vue:29
-#: src/components/StdDataDisplay/StdTable.vue:43
-#: src/components/StdDataEntry/compontents/StdSelector.vue:12
-#: src/views/domain/DomainList.vue:26
+#: src/components/StdDataDisplay/StdBatchEdit.vue:8
+#: src/components/StdDataDisplay/StdCurd.vue:28
+#: src/components/StdDataDisplay/StdTable.vue:53
+#: src/components/StdDataEntry/components/StdSelector.vue:12
+#: src/views/domain/DomainList.vue:25
 msgid "OK"
 msgstr ""
 
@@ -476,8 +630,8 @@ msgstr ""
 msgid "Password (*)"
 msgstr ""
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:24
-#: src/views/domain/ngx_conf/LocationEditor.vue:38
+#: src/views/domain/ngx_conf/LocationEditor.vue:34
+#: src/views/domain/ngx_conf/LocationEditor.vue:50
 msgid "Path"
 msgstr ""
 
@@ -495,6 +649,11 @@ msgstr ""
 msgid "Please input your username!"
 msgstr ""
 
+#: src/routes/index.ts:126
+#: src/views/preference/Preference.vue:2
+msgid "Preference"
+msgstr ""
+
 #: src/language/constants.ts:12
 msgid "Preparing lego configurations"
 msgstr ""
@@ -528,37 +687,56 @@ msgstr ""
 msgid "Reloading nginx"
 msgstr ""
 
+#: src/components/StdDataDisplay/StdTable.vue:10
 #: src/components/StdDataDisplay/StdTable.vue:15
+#: src/components/StdDataDisplay/StdTable.vue:9
 msgid "Reset"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:52
-#: src/views/domain/DomainEdit.vue:181
+#: src/views/preference/Preference.vue:8
+msgid "Run Mode"
+msgstr ""
+
+#: src/views/config/ConfigEdit.vue:70
+#: src/views/domain/DomainEdit.vue:190
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:33
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:34
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:40
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
+#: src/views/preference/Preference.vue:43
+#: src/views/preference/Preference.vue:44
 msgid "Save"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:32
-#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:33
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:34
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:35
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:36
 msgid "Save Directive"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:36
-#: src/views/domain/DomainAdd.vue:54
+#: src/views/config/ConfigEdit.vue:43
+#: src/views/domain/DomainAdd.vue:55
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:37
 msgid "Save error %{msg}"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdCurd.vue:102
+#: src/components/StdDataDisplay/StdBatchEdit.vue:40
+#: src/views/preference/Preference.vue:39
+msgid "Save successfully"
+msgstr ""
+
+#: src/components/StdDataDisplay/StdCurd.vue:108
 msgid "Save Successfully"
 msgstr ""
 
-#: src/views/config/ConfigEdit.vue:34
-#: src/views/domain/DomainAdd.vue:43
-#: src/views/domain/DomainEdit.vue:91
+#: src/views/config/ConfigEdit.vue:41
+#: src/views/domain/DomainAdd.vue:44
+#: src/views/domain/DomainEdit.vue:97
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:35
 msgid "Saved successfully"
 msgstr ""
 
-#: src/components/StdDataEntry/compontents/StdSelector.vue:13
+#: src/components/StdDataEntry/components/StdSelector.vue:13
 msgid "Selector"
 msgstr ""
 
@@ -567,15 +745,18 @@ msgstr ""
 msgid "Send"
 msgstr ""
 
-#: src/components/StdDataDisplay/StdTable.vue:140
-#: src/components/StdDataDisplay/StdTable.vue:298
-#: src/views/config/ConfigEdit.vue:22
-#: src/views/domain/DomainEdit.vue:56
-#: src/views/domain/DomainEdit.vue:68
-#: src/views/domain/DomainEdit.vue:77
-#: src/views/domain/DomainEdit.vue:94
-#: src/views/domain/DomainList.vue:78
+#: src/components/StdDataDisplay/StdBatchEdit.vue:43
+#: src/components/StdDataDisplay/StdTable.vue:168
+#: src/components/StdDataDisplay/StdTable.vue:343
+#: src/components/StdDataDisplay/StdTable.vue:463
+#: src/views/config/ConfigEdit.vue:29
+#: src/views/domain/DomainEdit.vue:100
+#: src/views/domain/DomainEdit.vue:62
+#: src/views/domain/DomainEdit.vue:74
+#: src/views/domain/DomainEdit.vue:83
+#: src/views/domain/DomainList.vue:83
 #: src/views/other/Install.vue:71
+#: src/views/preference/Preference.vue:41
 msgid "Server error"
 msgstr ""
 
@@ -583,33 +764,45 @@ msgstr ""
 msgid "Server Info"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:29
+#: src/views/domain/cert/IssueCert.vue:30
 msgid "server_name not found in directives"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:209
-#: src/views/domain/DomainAdd.vue:112
+#: src/views/domain/cert/IssueCert.vue:195
+#: src/views/domain/DomainAdd.vue:111
 msgid "server_name parameter is required"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:212
-#: src/views/domain/cert/IssueCert.vue:35
-msgid "server_name parameters more than one"
-msgstr ""
-
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:6
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:7
 msgid "Single Directive"
 msgstr ""
 
-#: src/routes/index.ts:107
+#: src/routes/index.ts:117
 msgid "Site Logs"
 msgstr ""
 
-#: src/routes/index.ts:51
+#: src/routes/index.ts:53
 msgid "Sites List"
 msgstr ""
 
-#: src/views/domain/DomainList.vue:19
+#: src/views/cert/Cert.vue:65
+msgid "SSL Certificate Key Path"
+msgstr ""
+
+#: src/views/cert/Cert.vue:58
+msgid "SSL Certificate Path"
+msgstr ""
+
+#: src/views/cert/Cert.vue:19
+msgid "SSL Certification Content"
+msgstr ""
+
+#: src/views/cert/Cert.vue:22
+msgid "SSL Certification Key Content"
+msgstr ""
+
+#: src/views/domain/DomainList.vue:24
 msgid "Status"
 msgstr ""
 
@@ -617,7 +810,7 @@ msgstr ""
 msgid "Storage"
 msgstr ""
 
-#: src/views/domain/cert/CertInfo.vue:16
+#: src/views/domain/cert/CertInfo.vue:11
 msgid "Subject Name: %{name}"
 msgstr ""
 
@@ -629,12 +822,16 @@ msgstr ""
 msgid "Table"
 msgstr ""
 
-#: src/routes/index.ts:85
+#: src/routes/index.ts:95
 #: src/views/pty/Terminal.vue:2
 msgid "Terminal"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:222
+#: src/views/preference/Preference.vue:14
+msgid "Terminal Start Command"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:207
 msgid "The certificate for the domain will be checked every hour, and will be renewed if it has been more than 1 month since it was last issued."
 msgstr ""
 
@@ -642,16 +839,33 @@ msgstr ""
 msgid "The filename cannot contain the following characters: %{c}"
 msgstr ""
 
+#: src/views/domain/cert/IssueCert.vue:203
+msgid "The server_name in the current configuration must be the domain name you need to get the certificate."
+msgstr ""
+
 #: src/language/constants.ts:6
 msgid "The username or password is incorrect"
 msgstr ""
 
-#: src/views/config/Config.vue:17
-#: src/views/domain/DomainList.vue:36
+#: src/views/preference/Preference.vue:20
+msgid "Theme"
+msgstr ""
+
+#: src/views/config/config.ts:14
+msgid "Type"
+msgstr ""
+
+#: src/views/cert/Cert.vue:72
+#: src/views/config/config.ts:29
+#: src/views/domain/DomainList.vue:41
 #: src/views/user/User.vue:37
 msgid "Updated at"
 msgstr ""
 
+#: src/components/StdDataDisplay/StdTable.vue:461
+msgid "Updated successfully"
+msgstr ""
+
 #: src/views/dashboard/DashBoard.vue:137
 msgid "Uptime:"
 msgstr ""
@@ -669,8 +883,14 @@ msgstr ""
 msgid "Using HTTP01 challenge provider"
 msgstr ""
 
-#: src/views/domain/cert/IssueCert.vue:26
-#: src/views/domain/DomainAdd.vue:24
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:10
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:13
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:9
+msgid "View"
+msgstr ""
+
+#: src/views/domain/cert/IssueCert.vue:27
+#: src/views/domain/DomainAdd.vue:22
 msgid "Warning"
 msgstr ""
 
@@ -688,8 +908,8 @@ msgstr ""
 msgid "Writing certificate to disk"
 msgstr ""
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:16
-#: src/views/domain/ngx_conf/LocationEditor.vue:10
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:21
+#: src/views/domain/ngx_conf/LocationEditor.vue:20
 msgid "Yes"
 msgstr ""
 

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
frontend/src/language/translations.json


BIN
frontend/src/language/zh_CN/app.mo


+ 343 - 137
frontend/src/language/zh_CN/app.po

@@ -10,23 +10,24 @@ msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Transfer-Encoding: 8bit\n"
 "Generated-By: easygettext\n"
-"X-Generator: Poedit 3.1.1\n"
+"X-Generator: Poedit 3.2.2\n"
 
-#: src/routes/index.ts:116
+#: src/routes/index.ts:134
 msgid "About"
 msgstr "关于"
 
-#: src/routes/index.ts:99 src/views/domain/ngx_conf/LogEntry.vue:64
+#: src/routes/index.ts:109 src/views/domain/ngx_conf/LogEntry.vue:64
 msgid "Access Logs"
 msgstr "访问日志"
 
-#: src/views/config/Config.vue:24 src/views/domain/DomainList.vue:42
-#: src/views/user/User.vue:43
+#: src/views/cert/Cert.vue:78 src/views/config/config.ts:36
+#: src/views/domain/DomainList.vue:47 src/views/user/User.vue:43
 msgid "Action"
 msgstr "操作"
 
-#: src/components/StdDataDisplay/StdCurd.vue:134
-#: src/components/StdDataDisplay/StdCurd.vue:26
+#: src/components/StdDataDisplay/StdCurd.vue:145
+#: src/components/StdDataDisplay/StdCurd.vue:25
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:26
 msgid "Add"
 msgstr "添加"
 
@@ -36,45 +37,73 @@ msgstr "添加"
 msgid "Add Directive Below"
 msgstr "在下面添加指令"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:33
-#: src/views/domain/ngx_conf/LocationEditor.vue:48
+#: src/views/domain/ngx_conf/LocationEditor.vue:45
+#: src/views/domain/ngx_conf/LocationEditor.vue:50
+#: src/views/domain/ngx_conf/LocationEditor.vue:51
+#: src/views/domain/ngx_conf/LocationEditor.vue:60
 msgid "Add Location"
 msgstr "添加 Location"
 
-#: src/routes/index.ts:55 src/views/domain/DomainAdd.vue:2
+#: src/routes/index.ts:57 src/views/domain/DomainAdd.vue:2
 msgid "Add Site"
 msgstr "添加站点"
 
-#: src/views/domain/DomainEdit.vue:19
+#: src/views/domain/DomainEdit.vue:18 src/views/domain/DomainEdit.vue:19
 msgid "Advance Mode"
 msgstr "高级模式"
 
-#: src/components/StdDataDisplay/StdTable.vue:44
-#: src/views/domain/DomainList.vue:27
-msgid "Are you sure you want to delete ?"
+#: src/components/StdDataDisplay/StdTable.vue:54
+#: src/views/domain/DomainList.vue:26
+msgid "Are you sure you want to delete?"
 msgstr "您确定要删除吗?"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:15
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:20
 msgid "Are you sure you want to remove this directive?"
 msgstr "您确定要删除这条指令?"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:9
+#: src/views/domain/ngx_conf/LocationEditor.vue:19
 msgid "Are you sure you want to remove this location?"
 msgstr "您确定要删除这个 Location?"
 
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:11
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:12
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:15
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:19
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:20
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:23
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:29
+msgid "Author"
+msgstr "作者"
+
+#: src/views/preference/Preference.vue:22
+#: src/views/preference/Preference.vue:23
+msgid "Auto"
+msgstr "自动"
+
+#: src/views/cert/Cert.vue:41
+msgid "Auto Cert"
+msgstr "自动更新"
+
+#: src/views/cert/Cert.vue:8
+msgid "Auto cert is enabled, please do not modify this certification."
+msgstr "自动更新已启用,请勿修改此证书配置。"
+
 #: src/views/nginx_log/NginxLog.vue:4
 msgid "Auto Refresh"
 msgstr "自动刷新"
 
-#: src/views/domain/cert/IssueCert.vue:78
+#: src/views/domain/cert/IssueCert.vue:71
 msgid "Auto-renewal disabled for %{name}"
 msgstr "成功关闭 %{name} 自动续签"
 
-#: src/views/domain/cert/IssueCert.vue:72
+#: src/views/domain/cert/IssueCert.vue:65
 msgid "Auto-renewal enabled for %{name}"
 msgstr "成功启用 %{name} 自动续签"
 
-#: src/views/domain/DomainEdit.vue:178 src/views/nginx_log/NginxLog.vue:172
+#: src/views/config/Config.vue:16 src/views/config/Config.vue:17
+#: src/views/config/Config.vue:27 src/views/config/Config.vue:5
+#: src/views/config/ConfigEdit.vue:64 src/views/domain/DomainEdit.vue:187
+#: src/views/nginx_log/NginxLog.vue:173
 msgid "Back"
 msgstr "返回"
 
@@ -86,40 +115,61 @@ msgstr "返回首页"
 msgid "Base information"
 msgstr "基本信息"
 
-#: src/views/domain/DomainEdit.vue:22
+#: src/views/domain/DomainEdit.vue:21 src/views/domain/DomainEdit.vue:22
 msgid "Basic Mode"
 msgstr "基本模式"
 
+#: src/components/StdDataDisplay/StdBatchEdit.vue:5
+#: src/components/StdDataDisplay/StdTable.vue:12
+#: src/components/StdDataDisplay/StdTable.vue:13
+#: src/components/StdDataDisplay/StdTable.vue:18
+msgid "Batch Modify"
+msgstr "批量修改"
+
 #: src/views/other/About.vue:21
 msgid "Build with"
 msgstr "构建基于"
 
-#: src/components/StdDataDisplay/StdCurd.vue:28
-#: src/components/StdDataEntry/compontents/StdSelector.vue:11
-#: src/views/config/ConfigEdit.vue:49
+#: src/components/StdDataDisplay/StdBatchEdit.vue:7
+#: src/components/StdDataDisplay/StdCurd.vue:27
+#: src/components/StdDataEntry/components/StdSelector.vue:11
 msgid "Cancel"
 msgstr "取消"
 
-#: src/views/domain/cert/CertInfo.vue:24
+#: src/views/domain/cert/CertInfo.vue:19
 msgid "Certificate has expired"
 msgstr "此证书已过期"
 
-#: src/views/domain/cert/CertInfo.vue:28
+#: src/views/domain/cert/CertInfo.vue:23
 msgid "Certificate is valid"
 msgstr "此证书有效"
 
-#: src/views/domain/cert/CertInfo.vue:14
+#: src/views/cert/Cert.vue:12 src/views/domain/cert/Cert.vue:35
 msgid "Certificate Status"
 msgstr "证书状态"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:29
-#: src/views/domain/ngx_conf/LocationEditor.vue:21
-#: src/views/domain/ngx_conf/LocationEditor.vue:35
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:175
+#: src/routes/index.ts:87 src/views/cert/Cert.vue:2
+msgid "Certification"
+msgstr "证书"
+
+#: src/views/domain/cert/ChangeCert.vue:2
+#: src/views/domain/cert/ChangeCert.vue:3
+#: src/views/domain/cert/ChangeCert.vue:5
+msgid "Change Certificate"
+msgstr "更改证书"
+
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:34
+#: src/views/domain/ngx_conf/LocationEditor.vue:31
+#: src/views/domain/ngx_conf/LocationEditor.vue:47
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:180
 msgid "Comments"
 msgstr "注释"
 
-#: src/views/domain/DomainAdd.vue:12
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:61
+msgid "Config Templates"
+msgstr "配置"
+
+#: src/views/domain/DomainAdd.vue:11
 msgid "Configuration Name"
 msgstr "配置名称"
 
@@ -131,8 +181,9 @@ msgstr "配置"
 msgid "Configure SSL"
 msgstr "配置 SSL"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:27
-#: src/views/domain/ngx_conf/LocationEditor.vue:41
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:37
+#: src/views/domain/ngx_conf/LocationEditor.vue:37
+#: src/views/domain/ngx_conf/LocationEditor.vue:53
 msgid "Content"
 msgstr "内容"
 
@@ -142,9 +193,9 @@ msgstr "CPU 状态"
 
 #: src/views/dashboard/DashBoard.vue:153
 msgid "CPU:"
-msgstr ""
+msgstr "CPU:"
 
-#: src/views/domain/DomainAdd.vue:150
+#: src/views/domain/DomainAdd.vue:149
 msgid "Create Another"
 msgstr "再创建一个"
 
@@ -156,7 +207,21 @@ msgstr "创建时间"
 msgid "Creating client facilitates communication with the CA server"
 msgstr "正在创建客户端用于与 CA 服务器通信"
 
-#: src/routes/index.ts:27
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:22
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:23
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:26
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:32
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:6
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:7
+msgid "Custom"
+msgstr "自定义"
+
+#: src/views/preference/Preference.vue:28
+#: src/views/preference/Preference.vue:29
+msgid "Dark"
+msgstr "深色"
+
+#: src/routes/index.ts:29
 msgid "Dashboard"
 msgstr "仪表盘"
 
@@ -164,41 +229,58 @@ msgstr "仪表盘"
 msgid "Database (Optional, default: database)"
 msgstr "数据库 (可选,默认: database)"
 
-#: src/components/StdDataDisplay/StdTable.vue:366
-#: src/views/domain/DomainList.vue:111
+#: src/components/StdDataDisplay/StdTable.vue:527
+#: src/views/domain/DomainList.vue:115
 msgid "Delete"
 msgstr "删除"
 
-#: src/components/StdDataDisplay/StdTable.vue:120
+#: src/components/StdDataDisplay/StdTable.vue:132
 msgid "Delete ID: %{id}"
 msgstr "删除 ID: %{id}"
 
-#: src/views/domain/DomainList.vue:76
+#: src/views/domain/DomainList.vue:81
 msgid "Delete site: %{site_name}"
 msgstr "删除站点: %{site_name}"
 
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:12
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:13
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:16
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:20
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:21
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:24
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:30
+msgid "Description"
+msgstr "描述"
+
 #: src/views/other/About.vue:7 src/views/other/About.vue:8
 msgid "Development Mode"
 msgstr "开发模式"
 
+#: src/views/config/config.ts:20
+msgid "Dir"
+msgstr "目录"
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:20
 msgid "Directive"
 msgstr "指令"
 
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:1
 #: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:2
 msgid "Directives"
 msgstr "指令"
 
-#: src/views/domain/cert/IssueCert.vue:80
+#: src/views/domain/cert/IssueCert.vue:73
 msgid "Disable auto-renewal failed for %{name}"
 msgstr "关闭 %{name} 自动续签失败"
 
-#: src/views/domain/DomainEdit.vue:10 src/views/domain/DomainList.vue:17
-#: src/views/domain/DomainList.vue:29
+#: src/views/cert/Cert.vue:51 src/views/domain/DomainEdit.vue:10
+#: src/views/domain/DomainEdit.vue:9 src/views/domain/DomainList.vue:16
+#: src/views/domain/DomainList.vue:34 src/views/domain/DomainList.vue:7
+#: src/views/domain/DomainList.vue:8 src/views/domain/DomainList.vue:9
 msgid "Disabled"
 msgstr "禁用"
 
-#: src/views/domain/DomainEdit.vue:112 src/views/domain/DomainList.vue:64
+#: src/views/domain/DomainEdit.vue:118 src/views/domain/DomainList.vue:69
 msgid "Disabled successfully"
 msgstr "禁用成功"
 
@@ -206,19 +288,23 @@ msgstr "禁用成功"
 msgid "Disk IO"
 msgstr "磁盘 IO"
 
-#: src/views/domain/DomainAdd.vue:60
+#: src/views/cert/Cert.vue:32
+msgid "Domain"
+msgstr "域名"
+
+#: src/views/domain/DomainAdd.vue:58
 msgid "Domain Config Created Successfully"
 msgstr "域名配置文件创建成功"
 
-#: src/views/domain/DomainEdit.vue:5
+#: src/views/domain/DomainEdit.vue:4 src/views/domain/DomainEdit.vue:5
 msgid "Edit %{n}"
 msgstr "编辑 %{n}"
 
-#: src/routes/index.ts:77 src/views/config/ConfigEdit.vue:2
+#: src/routes/index.ts:79 src/views/config/ConfigEdit.vue:2
 msgid "Edit Configuration"
 msgstr "编辑配置"
 
-#: src/routes/index.ts:59
+#: src/routes/index.ts:61
 msgid "Edit Site"
 msgstr "编辑站点"
 
@@ -226,11 +312,11 @@ msgstr "编辑站点"
 msgid "Email (*)"
 msgstr "邮箱 (*)"
 
-#: src/views/domain/cert/IssueCert.vue:74
+#: src/views/domain/cert/IssueCert.vue:67
 msgid "Enable auto-renewal failed for %{name}"
 msgstr "启用 %{name} 自动续签失败"
 
-#: src/views/domain/DomainAdd.vue:50
+#: src/views/domain/DomainAdd.vue:51
 msgid "Enable failed"
 msgstr "启用失败"
 
@@ -238,39 +324,43 @@ msgstr "启用失败"
 msgid "Enable TLS"
 msgstr "启用 TLS"
 
-#: src/views/domain/DomainEdit.vue:33 src/views/domain/DomainEdit.vue:7
-#: src/views/domain/DomainList.vue:12 src/views/domain/DomainList.vue:20
-#: src/views/domain/DomainList.vue:26
+#: src/views/cert/Cert.vue:48 src/views/domain/DomainEdit.vue:33
+#: src/views/domain/DomainEdit.vue:6 src/views/domain/DomainEdit.vue:7
+#: src/views/domain/DomainList.vue:10 src/views/domain/DomainList.vue:11
+#: src/views/domain/DomainList.vue:12 src/views/domain/DomainList.vue:19
+#: src/views/domain/DomainList.vue:31
 msgid "Enabled"
 msgstr "启用"
 
-#: src/views/domain/DomainAdd.vue:46 src/views/domain/DomainEdit.vue:103
-#: src/views/domain/DomainList.vue:54
+#: src/views/domain/DomainAdd.vue:47 src/views/domain/DomainEdit.vue:109
+#: src/views/domain/DomainList.vue:59
 msgid "Enabled successfully"
 msgstr "启用成功"
 
-#: src/views/domain/cert/IssueCert.vue:17
+#: src/views/domain/cert/IssueCert.vue:18
 msgid "Encrypt website with Let's Encrypt"
 msgstr "用 Let's Encrypt 对网站进行加密"
 
-#: src/routes/index.ts:103 src/views/domain/ngx_conf/LogEntry.vue:68
+#: src/routes/index.ts:113 src/views/domain/ngx_conf/LogEntry.vue:68
 msgid "Error Logs"
 msgstr "错误日志"
 
-#: src/views/domain/cert/CertInfo.vue:17
+#: src/views/domain/cert/CertInfo.vue:12
 msgid "Expiration Date: %{date}"
 msgstr "过期时间: %{date}"
 
 #: src/components/StdDataDisplay/StdTable.vue:12
-#: src/components/StdDataDisplay/StdTable.vue:317
+#: src/components/StdDataDisplay/StdTable.vue:362
+#: src/components/StdDataDisplay/StdTable.vue:6
+#: src/components/StdDataDisplay/StdTable.vue:7
 msgid "Export"
 msgstr "导出"
 
-#: src/views/domain/DomainEdit.vue:115 src/views/domain/DomainList.vue:68
+#: src/views/domain/DomainEdit.vue:121 src/views/domain/DomainList.vue:73
 msgid "Failed to disable %{msg}"
 msgstr "禁用失败 %{msg}"
 
-#: src/views/domain/DomainEdit.vue:106 src/views/domain/DomainList.vue:58
+#: src/views/domain/DomainEdit.vue:112 src/views/domain/DomainList.vue:63
 msgid "Failed to enable %{msg}"
 msgstr "启用失败 %{msg}"
 
@@ -278,6 +368,10 @@ msgstr "启用失败 %{msg}"
 msgid "Failed to get certificate information"
 msgstr "获取证书信息失败"
 
+#: src/views/config/config.ts:22
+msgid "File"
+msgstr "文件"
+
 #: src/views/other/Error.vue:3 src/views/other/Error.vue:4
 msgid "File Not Found"
 msgstr "未找到文件"
@@ -290,7 +384,19 @@ msgstr "过滤"
 msgid "Finished"
 msgstr "完成"
 
-#: src/components/StdDataEntry/compontents/StdPassword.vue:42
+#: src/views/config/ConfigEdit.vue:67
+msgid "Format Code"
+msgstr "代码格式化"
+
+#: src/views/config/ConfigEdit.vue:52
+msgid "Format error %{msg}"
+msgstr "保存错误 %{msg}"
+
+#: src/views/config/ConfigEdit.vue:50
+msgid "Format successfully"
+msgstr "保存成功"
+
+#: src/components/StdDataEntry/components/StdPassword.vue:42
 msgid "Generate"
 msgstr "生成"
 
@@ -298,15 +404,23 @@ msgstr "生成"
 msgid "Generating private key for registering account"
 msgstr "正在生成私钥用于注册账户"
 
-#: src/views/domain/cert/IssueCert.vue:103
+#: src/views/domain/cert/IssueCert.vue:96
 msgid "Getting the certificate, please wait..."
 msgstr "正在获取证书,请稍等..."
 
-#: src/routes/index.ts:20
+#: src/routes/index.ts:22
 msgid "Home"
 msgstr "首页"
 
-#: src/routes/index.ts:126 src/views/other/Install.vue:128
+#: src/views/preference/Preference.vue:17
+msgid "HTTP Challenge Port"
+msgstr "HTTP Challenge 监听端口"
+
+#: src/views/preference/Preference.vue:5
+msgid "HTTP Port"
+msgstr "HTTP 监听端口"
+
+#: src/routes/index.ts:144 src/views/other/Install.vue:128
 msgid "Install"
 msgstr "安装"
 
@@ -314,7 +428,7 @@ msgstr "安装"
 msgid "Install successfully"
 msgstr "安装成功"
 
-#: src/views/domain/cert/CertInfo.vue:15
+#: src/views/domain/cert/CertInfo.vue:10
 msgid "Intermediate Certification Authorities: %{issuer}"
 msgstr "中级证书颁发机构: %{issuer}"
 
@@ -322,23 +436,34 @@ msgstr "中级证书颁发机构: %{issuer}"
 msgid "Issued certificate successfully"
 msgstr "证书申请成功"
 
+#: src/views/preference/Preference.vue:11
+msgid "Jwt Secret"
+msgstr "Jwt 密钥"
+
 #: src/views/user/User.vue:26
 msgid "Leave blank for no change"
 msgstr "留空表示不修改"
 
+#: src/views/preference/Preference.vue:25
+#: src/views/preference/Preference.vue:26
+msgid "Light"
+msgstr "浅色"
+
 #: src/views/dashboard/DashBoard.vue:141
 msgid "Load Averages:"
 msgstr "系统负载:"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:5
+#: src/views/domain/ngx_conf/LocationEditor.vue:15
+#: src/views/domain/ngx_conf/LocationEditor.vue:8
+#: src/views/domain/ngx_conf/LocationEditor.vue:9
 msgid "Location"
 msgstr "Location"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:39
+#: src/views/domain/ngx_conf/LocationEditor.vue:40
 msgid "Locations"
 msgstr "Locations"
 
-#: src/routes/index.ts:132 src/views/other/Login.vue:103
+#: src/routes/index.ts:150 src/views/other/Login.vue:103
 msgid "Login"
 msgstr "登录"
 
@@ -350,7 +475,7 @@ msgstr "登录成功"
 msgid "Logout successful"
 msgstr "登出成功"
 
-#: src/views/domain/cert/IssueCert.vue:226
+#: src/views/domain/cert/IssueCert.vue:211
 msgid ""
 "Make sure you have configured a reverse proxy for .well-known directory to "
 "HTTPChallengePort (default: 9180) before getting the certificate."
@@ -358,15 +483,15 @@ msgstr ""
 "在获取签发证书前,请确保配置文件中已将 .well-known 目录反向代理到 "
 "HTTPChallengePort (默认: 9180)"
 
-#: src/routes/index.ts:68
+#: src/routes/index.ts:70
 msgid "Manage Configs"
 msgstr "配置管理"
 
-#: src/routes/index.ts:43 src/views/domain/DomainList.vue:2
+#: src/routes/index.ts:45 src/views/domain/DomainList.vue:2
 msgid "Manage Sites"
 msgstr "网站管理"
 
-#: src/routes/index.ts:35 src/views/user/User.vue:2
+#: src/routes/index.ts:37 src/views/user/User.vue:2
 msgid "Manage Users"
 msgstr "用户管理"
 
@@ -378,20 +503,26 @@ msgstr "内存"
 msgid "Memory and Storage"
 msgstr "内存与存储"
 
-#: src/components/StdDataDisplay/StdCurd.vue:26
-#: src/components/StdDataDisplay/StdTable.vue:18
-#: src/components/StdDataDisplay/StdTable.vue:19
-#: src/components/StdDataDisplay/StdTable.vue:24
-#: src/components/StdDataDisplay/StdTable.vue:34
-#: src/components/StdDataDisplay/StdTable.vue:36
+#: src/components/StdDataDisplay/StdCurd.vue:25
+#: src/components/StdDataDisplay/StdTable.vue:25
+#: src/components/StdDataDisplay/StdTable.vue:26
+#: src/components/StdDataDisplay/StdTable.vue:31
+#: src/components/StdDataDisplay/StdTable.vue:44
+#: src/components/StdDataDisplay/StdTable.vue:46
 msgid "Modify"
 msgstr "修改"
 
-#: src/views/domain/DomainAdd.vue:147
+#: src/views/domain/DomainAdd.vue:146
 msgid "Modify Config"
 msgstr "修改配置文件"
 
-#: src/views/config/Config.vue:12 src/views/domain/DomainList.vue:14
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:10
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:9
+msgid "Multi-line Directive"
+msgstr "单行指令"
+
+#: src/views/cert/Cert.vue:16 src/views/config/config.ts:9
+#: src/views/domain/DomainEdit.vue:36 src/views/domain/DomainList.vue:15
 msgid "Name"
 msgstr "名称"
 
@@ -411,49 +542,56 @@ msgstr "下载流量"
 msgid "Network Total Send"
 msgstr "上传流量"
 
-#: src/views/domain/DomainAdd.vue:137
+#: src/views/domain/DomainAdd.vue:136
 msgid "Next"
 msgstr "下一步"
 
-#: src/routes/index.ts:93 src/views/nginx_log/NginxLog.vue:2
+#: src/views/preference/Preference.vue:33
+msgid "Nginx Access Log Path"
+msgstr "Nginx 访问日志路径"
+
+#: src/views/preference/Preference.vue:36
+msgid "Nginx Error Log Path"
+msgstr "Nginx 错误日志路径"
+
+#: src/routes/index.ts:103 src/views/nginx_log/NginxLog.vue:2
 msgid "Nginx Log"
 msgstr "Nginx 日志"
 
-#: src/components/StdDataDisplay/StdTable.vue:42
-#: src/views/domain/DomainList.vue:25
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:17
-#: src/views/domain/ngx_conf/LocationEditor.vue:11
+#: src/components/StdDataDisplay/StdTable.vue:52
+#: src/views/domain/DomainList.vue:24
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:22
+#: src/views/domain/ngx_conf/LocationEditor.vue:21
 msgid "No"
 msgstr "取消"
 
-#: src/routes/index.ts:138 src/routes/index.ts:140
+#: src/routes/index.ts:156 src/routes/index.ts:158
 msgid "Not Found"
 msgstr "找不到页面"
 
-#: src/views/domain/cert/CertInfo.vue:19
+#: src/views/domain/cert/CertInfo.vue:14
 msgid "Not Valid Before: %{date}"
 msgstr "此前无效: %{date}"
 
-#: src/views/domain/cert/IssueCert.vue:218
-msgid ""
-"Note: The server_name in the current configuration must be the domain name "
-"you need to get the certificate."
-msgstr "注意:当前配置中的 server_name 必须为需要申请证书的域名。"
+#: src/views/domain/cert/IssueCert.vue:38
+msgid "Note"
+msgstr "注意"
 
 #: src/language/constants.ts:16 src/views/domain/cert/IssueCert.vue:3
 msgid "Obtaining certificate"
 msgstr "正在获取证书"
 
-#: src/components/StdDataDisplay/StdCurd.vue:29
-#: src/components/StdDataDisplay/StdTable.vue:43
-#: src/components/StdDataEntry/compontents/StdSelector.vue:12
-#: src/views/domain/DomainList.vue:26
+#: src/components/StdDataDisplay/StdBatchEdit.vue:8
+#: src/components/StdDataDisplay/StdCurd.vue:28
+#: src/components/StdDataDisplay/StdTable.vue:53
+#: src/components/StdDataEntry/components/StdSelector.vue:12
+#: src/views/domain/DomainList.vue:25
 msgid "OK"
 msgstr "确定"
 
 #: src/views/dashboard/DashBoard.vue:147
 msgid "OS:"
-msgstr ""
+msgstr "OS:"
 
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:22
 msgid "Params"
@@ -467,8 +605,8 @@ msgstr "密码"
 msgid "Password (*)"
 msgstr "密码 (*)"
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:24
-#: src/views/domain/ngx_conf/LocationEditor.vue:38
+#: src/views/domain/ngx_conf/LocationEditor.vue:34
+#: src/views/domain/ngx_conf/LocationEditor.vue:50
 msgid "Path"
 msgstr "路径"
 
@@ -484,6 +622,10 @@ msgstr "请输入您的密码!"
 msgid "Please input your username!"
 msgstr "请输入您的用户名!"
 
+#: src/routes/index.ts:126 src/views/preference/Preference.vue:2
+msgid "Preference"
+msgstr "偏好设置"
+
 #: src/language/constants.ts:12
 msgid "Preparing lego configurations"
 msgstr "正在准备 Lego 的配置"
@@ -516,34 +658,53 @@ msgstr "正在注册用户"
 msgid "Reloading nginx"
 msgstr "正在重载 Nginx"
 
+#: src/components/StdDataDisplay/StdTable.vue:10
 #: src/components/StdDataDisplay/StdTable.vue:15
+#: src/components/StdDataDisplay/StdTable.vue:9
 msgid "Reset"
 msgstr "重置"
 
-#: src/views/config/ConfigEdit.vue:52 src/views/domain/DomainEdit.vue:181
+#: src/views/preference/Preference.vue:8
+msgid "Run Mode"
+msgstr "运行模式"
+
+#: src/views/config/ConfigEdit.vue:70 src/views/domain/DomainEdit.vue:190
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:33
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:34
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:40
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:41
+#: src/views/preference/Preference.vue:43
+#: src/views/preference/Preference.vue:44
 msgid "Save"
 msgstr "保存"
 
-#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:32
-#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:33
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:34
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:35
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:36
 msgid "Save Directive"
 msgstr "保存指令"
 
-#: src/views/config/ConfigEdit.vue:36 src/views/domain/DomainAdd.vue:54
+#: src/views/config/ConfigEdit.vue:43 src/views/domain/DomainAdd.vue:55
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:37
 msgid "Save error %{msg}"
 msgstr "保存错误 %{msg}"
 
-#: src/components/StdDataDisplay/StdCurd.vue:102
+#: src/components/StdDataDisplay/StdBatchEdit.vue:40
+#: src/views/preference/Preference.vue:39
+msgid "Save successfully"
+msgstr "保存成功"
+
+#: src/components/StdDataDisplay/StdCurd.vue:108
 msgid "Save Successfully"
 msgstr "保存成功"
 
-#: src/views/config/ConfigEdit.vue:34 src/views/domain/DomainAdd.vue:43
-#: src/views/domain/DomainEdit.vue:91
+#: src/views/config/ConfigEdit.vue:41 src/views/domain/DomainAdd.vue:44
+#: src/views/domain/DomainEdit.vue:97
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:35
 msgid "Saved successfully"
 msgstr "保存成功"
 
-#: src/components/StdDataEntry/compontents/StdSelector.vue:13
+#: src/components/StdDataEntry/components/StdSelector.vue:13
 msgid "Selector"
 msgstr "选择器"
 
@@ -551,12 +712,14 @@ msgstr "选择器"
 msgid "Send"
 msgstr "上传"
 
-#: src/components/StdDataDisplay/StdTable.vue:140
-#: src/components/StdDataDisplay/StdTable.vue:298
-#: src/views/config/ConfigEdit.vue:22 src/views/domain/DomainEdit.vue:56
-#: src/views/domain/DomainEdit.vue:68 src/views/domain/DomainEdit.vue:77
-#: src/views/domain/DomainEdit.vue:94 src/views/domain/DomainList.vue:78
-#: src/views/other/Install.vue:71
+#: src/components/StdDataDisplay/StdBatchEdit.vue:43
+#: src/components/StdDataDisplay/StdTable.vue:168
+#: src/components/StdDataDisplay/StdTable.vue:343
+#: src/components/StdDataDisplay/StdTable.vue:463
+#: src/views/config/ConfigEdit.vue:29 src/views/domain/DomainEdit.vue:100
+#: src/views/domain/DomainEdit.vue:62 src/views/domain/DomainEdit.vue:74
+#: src/views/domain/DomainEdit.vue:83 src/views/domain/DomainList.vue:83
+#: src/views/other/Install.vue:71 src/views/preference/Preference.vue:41
 msgid "Server error"
 msgstr "服务器错误"
 
@@ -564,32 +727,44 @@ msgstr "服务器错误"
 msgid "Server Info"
 msgstr "服务器信息"
 
-#: src/views/domain/cert/IssueCert.vue:29
+#: src/views/domain/cert/IssueCert.vue:30
 msgid "server_name not found in directives"
 msgstr "未在指令集合中找到 server_name"
 
-#: src/views/domain/cert/IssueCert.vue:209 src/views/domain/DomainAdd.vue:112
+#: src/views/domain/cert/IssueCert.vue:195 src/views/domain/DomainAdd.vue:111
 msgid "server_name parameter is required"
 msgstr "必须为 server_name 指令指明参数"
 
-#: src/views/domain/cert/IssueCert.vue:212
-#: src/views/domain/cert/IssueCert.vue:35
-msgid "server_name parameters more than one"
-msgstr "server_name 指令包含多个参数"
-
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:6
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:7
 msgid "Single Directive"
 msgstr "单行指令"
 
-#: src/routes/index.ts:107
+#: src/routes/index.ts:117
 msgid "Site Logs"
 msgstr "站点列表"
 
-#: src/routes/index.ts:51
+#: src/routes/index.ts:53
 msgid "Sites List"
 msgstr "站点列表"
 
-#: src/views/domain/DomainList.vue:19
+#: src/views/cert/Cert.vue:65
+msgid "SSL Certificate Key Path"
+msgstr "SSL证书密钥路径"
+
+#: src/views/cert/Cert.vue:58
+msgid "SSL Certificate Path"
+msgstr "SSL证书路径"
+
+#: src/views/cert/Cert.vue:19
+msgid "SSL Certification Content"
+msgstr "SSL证书内容"
+
+#: src/views/cert/Cert.vue:22
+msgid "SSL Certification Key Content"
+msgstr "SSL证书密钥内容"
+
+#: src/views/domain/DomainList.vue:24
 msgid "Status"
 msgstr "状态"
 
@@ -597,23 +772,27 @@ msgstr "状态"
 msgid "Storage"
 msgstr "存储"
 
-#: src/views/domain/cert/CertInfo.vue:16
+#: src/views/domain/cert/CertInfo.vue:11
 msgid "Subject Name: %{name}"
 msgstr "主体名称: %{name}"
 
 #: src/views/dashboard/DashBoard.vue:36
 msgid "Swap"
-msgstr ""
+msgstr "Swap"
 
 #: src/components/StdDataDisplay/StdCurd.vue:3
 msgid "Table"
 msgstr "列表"
 
-#: src/routes/index.ts:85 src/views/pty/Terminal.vue:2
+#: src/routes/index.ts:95 src/views/pty/Terminal.vue:2
 msgid "Terminal"
 msgstr "终端"
 
-#: src/views/domain/cert/IssueCert.vue:222
+#: src/views/preference/Preference.vue:14
+msgid "Terminal Start Command"
+msgstr "终端启动命令"
+
+#: src/views/domain/cert/IssueCert.vue:207
 msgid ""
 "The certificate for the domain will be checked every hour, and will be "
 "renewed if it has been more than 1 month since it was last issued."
@@ -624,15 +803,33 @@ msgstr ""
 msgid "The filename cannot contain the following characters: %{c}"
 msgstr "文件名不能包含以下字符: %{c}"
 
+#: src/views/domain/cert/IssueCert.vue:203
+msgid ""
+"The server_name in the current configuration must be the domain name you "
+"need to get the certificate."
+msgstr "当前配置中的 server_name 必须为需要申请证书的域名。"
+
 #: src/language/constants.ts:6
 msgid "The username or password is incorrect"
 msgstr "用户名或密码错误"
 
-#: src/views/config/Config.vue:17 src/views/domain/DomainList.vue:36
-#: src/views/user/User.vue:37
+#: src/views/preference/Preference.vue:20
+msgid "Theme"
+msgstr "主题"
+
+#: src/views/config/config.ts:14
+msgid "Type"
+msgstr "类型"
+
+#: src/views/cert/Cert.vue:72 src/views/config/config.ts:29
+#: src/views/domain/DomainList.vue:41 src/views/user/User.vue:37
 msgid "Updated at"
 msgstr "修改时间"
 
+#: src/components/StdDataDisplay/StdTable.vue:461
+msgid "Updated successfully"
+msgstr "更新成功"
+
 #: src/views/dashboard/DashBoard.vue:137
 msgid "Uptime:"
 msgstr "运行时间:"
@@ -649,7 +846,13 @@ msgstr "用户名 (*)"
 msgid "Using HTTP01 challenge provider"
 msgstr "使用 HTTP01 challenge provider"
 
-#: src/views/domain/cert/IssueCert.vue:26 src/views/domain/DomainAdd.vue:24
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:10
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:13
+#: src/views/domain/ngx_conf/ConfigTemplate.vue:9
+msgid "View"
+msgstr "查看"
+
+#: src/views/domain/cert/IssueCert.vue:27 src/views/domain/DomainAdd.vue:22
 msgid "Warning"
 msgstr "警告"
 
@@ -666,8 +869,8 @@ msgstr "正在将证书私钥写入磁盘"
 msgid "Writing certificate to disk"
 msgstr "正在将证书写入磁盘"
 
-#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:16
-#: src/views/domain/ngx_conf/LocationEditor.vue:10
+#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:21
+#: src/views/domain/ngx_conf/LocationEditor.vue:20
 msgid "Yes"
 msgstr "是的"
 
@@ -676,6 +879,12 @@ msgctxt "Project"
 msgid "License"
 msgstr "开源许可"
 
+#~ msgid "Are you sure you want to delete ?"
+#~ msgstr "您确定要删除吗?"
+
+#~ msgid "server_name parameters more than one"
+#~ msgstr "server_name 指令包含多个参数"
+
 #~ msgid "All logs"
 #~ msgstr "所有日志"
 
@@ -721,9 +930,6 @@ msgstr "开源许可"
 #~ msgid "Certificate Path (ssl_certificate)"
 #~ msgstr "TLS 证书路径 (ssl_certificate)"
 
-#~ msgid "HTTP Listen Port"
-#~ msgstr "HTTP 监听端口"
-
 #~ msgid "HTTPS Listen Port"
 #~ msgstr "HTTPS 监听端口"
 

BIN
frontend/src/language/zh_TW/app.mo


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 391 - 178
frontend/src/language/zh_TW/app.po


+ 32 - 28
frontend/src/layouts/BaseLayout.vue

@@ -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
@@ -122,37 +122,36 @@ const lang = computed(() => {
 </style>
 
 <style lang="less">
-@dark: ~"(prefers-color-scheme: dark)";
-
 body {
     overflow: unset !important;
 }
 
-@media @dark {
+.dark {
     h1, h2, h3, h4, h5, h6, p {
         color: #fafafa !important;
     }
 
-}
+    .ant-checkbox-indeterminate {
+        .ant-checkbox-inner {
+            background-color: transparent !important;
+        }
+    }
 
-.ant-layout-header {
-    padding: 0 !important;
-    background-color: #fff !important;
-    @media @dark {
+    .ant-menu {
+        background: unset !important;
+    }
+
+    .ant-layout-header {
         background-color: #1f1f1f !important;
     }
-}
 
-.ant-card {
-    @media @dark {
+    .ant-card {
         background-color: #1f1f1f !important;
     }
-}
 
-.ant-layout-sider {
-    background-color: #ffffff;
-    @media @dark {
+    .ant-layout-sider {
         background-color: rgb(20, 20, 20) !important;
+
         .ant-layout-sider-trigger {
             background-color: rgb(20, 20, 20) !important;
         }
@@ -160,8 +159,25 @@ body {
         .ant-menu {
             border-right: 0 !important;
         }
+
+        &.ant-layout-sider-has-trigger {
+            padding-bottom: 0;
+        }
+
+        box-shadow: 2px 0 8px rgba(29, 35, 41, 0.05);
     }
 
+}
+
+.ant-layout-header {
+    padding: 0 !important;
+    background-color: #fff !important;
+}
+
+
+.ant-layout-sider {
+    background-color: #ffffff;
+
     &.ant-layout-sider-has-trigger {
         padding-bottom: 0;
     }
@@ -179,18 +195,6 @@ body {
     }
 }
 
-@media @dark {
-    .ant-checkbox-indeterminate {
-        .ant-checkbox-inner {
-            background-color: transparent !important;
-        }
-    }
-
-    .ant-menu {
-        background: unset !important;
-    }
-
-}
 
 .ant-table-small {
     font-size: 13px;

+ 1 - 1
frontend/src/layouts/HeaderLayout.vue

@@ -55,7 +55,7 @@ function logout() {
     }
 }
 
-@media (prefers-color-scheme: dark) {
+.dark {
     .header {
         box-shadow: 1px 1px 0 0 #404040;
 

+ 1 - 0
frontend/src/lib/theme/index.ts

@@ -13,6 +13,7 @@ function changeTheme(theme: string) {
 }
 
 export const dark_mode = async (enabled: Boolean) => {
+    document.body.setAttribute('class', enabled ? 'dark' : 'light')
     if (enabled) {
         changeTheme((await import('@/dark.less?inline')).default)
         changeCss('--page-bg-color', '#141414')

+ 4 - 0
frontend/src/pinia/moudule/settings.ts

@@ -4,6 +4,7 @@ export const useSettingsStore = defineStore('settings', {
     state: () => ({
         language: '',
         theme: 'light',
+        preference_theme: 'auto'
     }),
     getters: {},
     actions: {
@@ -12,6 +13,9 @@ export const useSettingsStore = defineStore('settings', {
         },
         set_theme(t: string) {
             this.theme = t
+        },
+        set_preference_theme(t: string) {
+            this.preference_theme = t
         }
     },
     persist: true

+ 30 - 12
frontend/src/routes/index.ts

@@ -9,7 +9,9 @@ import {
     HomeOutlined,
     InfoCircleOutlined,
     UserOutlined,
-    FileTextOutlined
+    FileTextOutlined,
+    SettingOutlined,
+    SafetyCertificateOutlined
 } from '@ant-design/icons-vue'
 
 const {$gettext} = gettext
@@ -36,7 +38,7 @@ export const routes = [
                 component: () => import('@/views/user/User.vue'),
                 meta: {
                     icon: UserOutlined
-                },
+                }
             },
             {
                 path: 'domain',
@@ -49,11 +51,11 @@ export const routes = [
                 children: [{
                     path: 'list',
                     name: () => $gettext('Sites List'),
-                    component: () => import('@/views/domain/DomainList.vue'),
+                    component: () => import('@/views/domain/DomainList.vue')
                 }, {
                     path: 'add',
                     name: () => $gettext('Add Site'),
-                    component: () => import('@/views/domain/DomainAdd.vue'),
+                    component: () => import('@/views/domain/DomainAdd.vue')
                 }, {
                     path: ':name',
                     name: () => $gettext('Edit Site'),
@@ -61,7 +63,7 @@ export const routes = [
                     meta: {
                         hiddenInSidebar: true
                     }
-                },]
+                }]
             },
             {
                 path: 'config',
@@ -73,12 +75,20 @@ export const routes = [
                 }
             },
             {
-                path: 'config/:name',
+                path: 'config/:name+/edit',
                 name: () => $gettext('Edit Configuration'),
                 component: () => import('@/views/config/ConfigEdit.vue'),
                 meta: {
                     hiddenInSidebar: true
-                },
+                }
+            },
+            {
+                path: 'cert',
+                name: () => $gettext('Certification'),
+                component: () => import('@/views/cert/Cert.vue'),
+                meta: {
+                    icon: SafetyCertificateOutlined
+                }
             },
             {
                 path: 'terminal',
@@ -97,20 +107,28 @@ export const routes = [
                 children: [{
                     path: 'access',
                     name: () => $gettext('Access Logs'),
-                    component: () => import('@/views/nginx_log/NginxLog.vue'),
+                    component: () => import('@/views/nginx_log/NginxLog.vue')
                 }, {
                     path: 'error',
                     name: () => $gettext('Error Logs'),
-                    component: () => import('@/views/nginx_log/NginxLog.vue'),
+                    component: () => import('@/views/nginx_log/NginxLog.vue')
                 }, {
                     path: 'site',
                     name: () => $gettext('Site Logs'),
                     component: () => import('@/views/nginx_log/NginxLog.vue'),
                     meta: {
                         hiddenInSidebar: true
-                    },
+                    }
                 }]
             },
+            {
+                path: 'preference',
+                name: () => $gettext('Preference'),
+                component: () => import('@/views/preference/Preference.vue'),
+                meta: {
+                    icon: SettingOutlined
+                }
+            },
             {
                 path: 'about',
                 name: () => $gettext('About'),
@@ -118,7 +136,7 @@ export const routes = [
                 meta: {
                     icon: InfoCircleOutlined
                 }
-            },
+            }
         ]
     },
     {
@@ -144,7 +162,7 @@ export const routes = [
 const router = createRouter({
     history: createWebHistory(),
     // @ts-ignore
-    routes: routes,
+    routes: routes
 })
 
 router.beforeEach((to, from, next) => {

+ 1 - 1
frontend/src/version.json

@@ -1 +1 @@
-{"version":"1.6.8","build_id":57,"total_build":127}
+{"version":"1.7.0","build_id":63,"total_build":133}

+ 114 - 0
frontend/src/views/cert/Cert.vue

@@ -0,0 +1,114 @@
+<script setup lang="tsx">
+import {useGettext} from 'vue3-gettext'
+import {input} from '@/components/StdDataEntry'
+import {customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+import {h} from 'vue'
+import {Badge} from 'ant-design-vue'
+import cert from '@/api/cert'
+import StdCurd from '@/components/StdDataDisplay/StdCurd.vue'
+import Template from '@/views/template/Template.vue'
+import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
+import CertInfo from '@/views/domain/cert/CertInfo.vue'
+
+const {$gettext} = useGettext()
+
+const columns = [{
+    title: () => $gettext('Name'),
+    dataIndex: 'name',
+    sorter: true,
+    pithy: true,
+    customRender: (args: customRender) => {
+        const {text, record} = args
+        if (!text) {
+            return h('div', record.domain)
+        }
+        return h('div', text)
+    },
+    edit: {
+        type: input
+    },
+    search: true
+}, {
+    title: () => $gettext('Domain'),
+    dataIndex: 'domain',
+    sorter: true,
+    pithy: true,
+    edit: {
+        type: input
+    },
+    search: true
+}, {
+    title: () => $gettext('Auto Cert'),
+    dataIndex: 'auto_cert',
+    customRender: (args: customRender) => {
+        const template: any = []
+        const {text, column} = args
+        if (text === true || text > 0) {
+            template.push(<Badge status="success"/>)
+            template.push($gettext('Enabled'))
+        } else {
+            template.push(<Badge status="warning"/>)
+            template.push($gettext('Disabled'))
+        }
+        return h('div', template)
+    },
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('SSL Certificate Path'),
+    dataIndex: 'ssl_certificate_path',
+    edit: {
+        type: input
+    },
+    display: false
+}, {
+    title: () => $gettext('SSL Certificate Key Path'),
+    dataIndex: 'ssl_certificate_key_path',
+    edit: {
+        type: input
+    },
+    display: false
+}, {
+    title: () => $gettext('Updated at'),
+    dataIndex: 'updated_at',
+    customRender: datetime,
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Action'),
+    dataIndex: 'action'
+}]
+</script>
+
+<template>
+    <std-curd :title="$gettext('Certification')" :api="cert" :columns="columns"
+              row-key="name"
+    >
+        <template #beforeEdit="{data}">
+            <div v-if="data.auto_cert===1" style="margin-bottom: 15px">
+                <a-alert
+                    :message="$gettext('Auto cert is enabled, please do not modify this certification.')" type="info"
+                    show-icon/>
+            </div>
+            <a-form layout="vertical" v-if="data.certificate_info">
+                <a-form-item :label="$gettext('Certificate Status')">
+                    <cert-info :cert="data.certificate_info"/>
+                </a-form-item>
+            </a-form>
+        </template>
+        <template #edit="{data}">
+            <a-form layout="vertical">
+                <a-form-item :label="$gettext('SSL Certification Content')">
+                    <code-editor v-model:content="data.ssl_certification" default-height="200px"/>
+                </a-form-item>
+                <a-form-item :label="$gettext('SSL Certification Key Content')">
+                    <code-editor v-model:content="data.ssl_certification_key" default-height="200px"/>
+                </a-form-item>
+            </a-form>
+        </template>
+    </std-curd>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 47 - 22
frontend/src/views/config/Config.vue

@@ -2,43 +2,68 @@
 import StdTable from '@/components/StdDataDisplay/StdTable.vue'
 import gettext from '@/gettext'
 import config from '@/api/config'
-import {datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+import {customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+import {computed, h, nextTick, ref, watch} from 'vue'
 
 const {$gettext} = gettext
 
 const api = config
 
-const columns = [{
-    title: () => $gettext('Name'),
-    dataIndex: 'name',
-    sorter: true,
-    pithy: true
-}, {
-    title: () => $gettext('Updated at'),
-    dataIndex: 'modify',
-    customRender: datetime,
-    datetime: true,
-    sorter: true,
-    pithy: true
-}, {
-    title: () => $gettext('Action'),
-    dataIndex: 'action'
-}]
+import configColumns from '@/views/config/config'
+import {useRoute} from 'vue-router'
+import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
+import router from '@/routes'
+
+const table = ref(null)
+const route = useRoute()
+
+const basePath = computed(() => {
+    let dir = route?.query?.dir ?? ''
+    if (dir) dir += '/'
+    return dir
+})
+
+const get_params = computed(() => {
+    return {
+        dir: basePath.value
+    }
+})
+
+const update = ref(1)
+
+watch(get_params, () => {
+    update.value++
+})
 </script>
+
 <template>
     <a-card :title="$gettext('Configurations')">
         <std-table
+            :key="update"
+            ref="table"
             :api="api"
-            :columns="columns"
+            :columns="configColumns"
             :deletable="false"
             :disable_search="true"
             row-key="name"
-            @clickEdit="r => {
-                $router.push({
-                    path: '/config/' + r
-                })
+            :get_params="get_params"
+            @clickEdit="(r, row) => {
+                if (!row.is_dir) {
+                    $router.push({
+                        path: '/config/' + basePath + r + '/edit'
+                    })
+                } else {
+                    $router.push({
+                        query: {
+                            dir: basePath + r
+                        }
+                    })
+                }
             }"
         />
+        <footer-tool-bar v-if="basePath">
+            <a-button @click="router.go(-1)">{{ $gettext('Back') }}</a-button>
+        </footer-tool-bar>
     </a-card>
 </template>
 

+ 21 - 3
frontend/src/views/config/ConfigEdit.vue

@@ -2,15 +2,22 @@
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
 import gettext from '@/gettext'
 import {useRoute} from 'vue-router'
-import {ref} from 'vue'
+import {computed, ref} from 'vue'
 import config from '@/api/config'
 import {message} from 'ant-design-vue'
 import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
+import ngx from '@/api/ngx'
 
 const {$gettext, interpolate} = gettext
 const route = useRoute()
 
-const name = ref(route.params.name)
+const name = computed(() => {
+    const n = route.params.name
+    if (typeof n === 'string') {
+        return n
+    }
+    return n?.join('/')
+})
 
 const configText = ref('')
 
@@ -37,6 +44,14 @@ function save() {
     })
 }
 
+function format_code() {
+    ngx.format_code(configText.value).then(r => {
+        configText.value = r.content
+        message.success($gettext('Format successfully'))
+    }).catch(r => {
+        message.error(interpolate($gettext('Format error %{msg}'), {msg: r.message ?? ''}))
+    })
+}
 </script>
 
 
@@ -46,7 +61,10 @@ function save() {
         <footer-tool-bar>
             <a-space>
                 <a-button @click="$router.go(-1)">
-                    <translate>Cancel</translate>
+                    <translate>Back</translate>
+                </a-button>
+                <a-button @click="format_code">
+                    <translate>Format Code</translate>
                 </a-button>
                 <a-button type="primary" @click="save">
                     <translate>Save</translate>

+ 40 - 0
frontend/src/views/config/config.ts

@@ -0,0 +1,40 @@
+import {customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+import gettext from '@/gettext'
+
+const {$gettext} = gettext
+
+import {h} from 'vue'
+
+const configColumns = [{
+    title: () => $gettext('Name'),
+    dataIndex: 'name',
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Type'),
+    dataIndex: 'is_dir',
+    customRender: (args: customRender) => {
+        const template: any = []
+        const {text, column} = args
+        if (text === true || text > 0) {
+            template.push($gettext('Dir'))
+        } else {
+            template.push($gettext('File'))
+        }
+        return h('div', template)
+    },
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Updated at'),
+    dataIndex: 'modify',
+    customRender: datetime,
+    datetime: true,
+    sorter: true,
+    pithy: true
+}, {
+    title: () => $gettext('Action'),
+    dataIndex: 'action'
+}]
+
+export default configColumns

+ 2 - 3
frontend/src/views/domain/DomainAdd.vue

@@ -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"/>

+ 14 - 5
frontend/src/views/domain/DomainEdit.vue

@@ -4,8 +4,8 @@ import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
 
 import NgxConfigEditor from '@/views/domain/ngx_conf/NgxConfigEditor'
 import {useGettext} from 'vue3-gettext'
-import {reactive, ref} from 'vue'
-import {useRoute} from 'vue-router'
+import {reactive, ref, watch} from 'vue'
+import {useRoute, useRouter} from 'vue-router'
 import domain from '@/api/domain'
 import ngx from '@/api/ngx'
 import {message} from 'ant-design-vue'
@@ -14,8 +14,13 @@ import {message} from 'ant-design-vue'
 const {$gettext, interpolate} = useGettext()
 
 const route = useRoute()
+const router = useRouter()
 
 const name = ref(route.params.name.toString())
+watch(route, () => {
+    name.value = route.params?.name?.toString() ?? ''
+})
+
 const update = ref(0)
 
 const ngx_config = reactive({
@@ -32,6 +37,7 @@ const configText = ref('')
 const ok = ref(false)
 const advance_mode = ref(false)
 const saving = ref(false)
+const filename = ref('')
 
 init()
 
@@ -40,7 +46,7 @@ function handle_response(r: any) {
     Object.keys(cert_info_map).forEach(v => {
         delete cert_info_map[v]
     })
-
+    filename.value = r.name
     configText.value = r.config
     enabled.value = r.enabled
     auto_cert.value = r.auto_cert
@@ -85,9 +91,9 @@ const save = async () => {
         await build_config()
     }
 
-    domain.save(name.value, {content: configText.value}).then(r => {
+    domain.save(name.value, {name: filename.value, content: configText.value}).then(r => {
         handle_response(r)
-
+        router.push('/domain/' + filename.value)
         message.success($gettext('Saved successfully'))
 
     }).catch((e: any) => {
@@ -159,6 +165,9 @@ function on_change_enabled(checked: boolean) {
                     <a-form-item :label="$gettext('Enabled')">
                         <a-switch v-model:checked="enabled" @change="on_change_enabled"/>
                     </a-form-item>
+                    <a-form-item :label="$gettext('Name')">
+                        <a-input v-model:value="filename"/>
+                    </a-form-item>
                     <ngx-config-editor
                         ref="ngx_config_editor"
                         :ngx_config="ngx_config"

+ 2 - 2
frontend/src/views/domain/DomainList.vue

@@ -30,7 +30,7 @@ const columns = [{
             template.push(<Badge status="success"/>)
             template.push($gettext('Enabled'))
         } else {
-            template.push(<Badge status="error"/>)
+            template.push(<Badge status="warning"/>)
             template.push($gettext('Disabled'))
         }
         return h('div', template)
@@ -110,7 +110,7 @@ function destroy(site_name: any) {
                     <a-popconfirm
                         :cancelText="$gettext('No')"
                         :okText="$gettext('OK')"
-                        :title="$gettext('Are you sure you want to delete ?')"
+                        :title="$gettext('Are you sure you want to delete?')"
                         @confirm="destroy(record['name'])">
                         <a v-translate>Delete</a>
                     </a-popconfirm>

+ 7 - 0
frontend/src/views/domain/cert/Cert.vue

@@ -2,6 +2,10 @@
 import CertInfo from '@/views/domain/cert/CertInfo.vue'
 import IssueCert from '@/views/domain/cert/IssueCert.vue'
 import {computed, ref} from 'vue'
+import {useGettext} from 'vue3-gettext'
+import ChangeCert from '@/views/domain/cert/ChangeCert.vue'
+
+const {$gettext} = useGettext()
 
 const props = defineProps(['directivesMap', 'current_server_directives', 'enabled', 'cert_info'])
 
@@ -28,8 +32,11 @@ const enabled = computed({
 
 <template>
     <div>
+        <h2 v-translate>Certificate Status</h2>
         <cert-info ref="info" :cert="props.cert_info"/>
 
+        <change-cert :directives-map="props.directivesMap"/>
+
         <issue-cert
             :current_server_directives="props.current_server_directives"
             :directives-map="props.directivesMap"

+ 0 - 5
frontend/src/views/domain/cert/CertInfo.vue

@@ -1,17 +1,12 @@
 <script setup lang="ts">
 import {CloseCircleOutlined, CheckCircleOutlined} from '@ant-design/icons-vue'
 import dayjs from 'dayjs'
-import {reactive, ref} from 'vue'
-import domain from '@/api/domain'
 
 const props = defineProps(['cert'])
-
-const cert = props.cert
 </script>
 
 <template>
     <div class="cert-info" v-if="cert">
-        <h2 v-translate>Certificate Status</h2>
         <p v-translate="{issuer: cert.issuer_name}">Intermediate Certification Authorities: %{issuer}</p>
         <p v-translate="{name: cert.subject_name}">Subject Name: %{name}</p>
         <p v-translate="{date: dayjs(cert.not_after).format('YYYY-MM-DD HH:mm:ss').toString()}">

+ 90 - 0
frontend/src/views/domain/cert/ChangeCert.vue

@@ -0,0 +1,90 @@
+<script setup lang="tsx">
+import {useGettext} from 'vue3-gettext'
+import {h, ref} from 'vue'
+import StdTable from '@/components/StdDataDisplay/StdTable.vue'
+import cert from '@/api/cert'
+import {customRender, datetime} from '@/components/StdDataDisplay/StdTableTransformer'
+import {input} from '@/components/StdDataEntry'
+import {Badge} from 'ant-design-vue'
+
+const {$gettext} = useGettext()
+
+const props = defineProps(['directivesMap'])
+
+const visible = ref(false)
+
+const record: any = ref({})
+
+const columns = [{
+    title: () => $gettext('Name'),
+    dataIndex: 'name',
+    sorter: true,
+    pithy: true,
+    customRender: (args: customRender) => {
+        const {text, record} = args
+        if (!text) {
+            return h('div', record.domain)
+        }
+        return h('div', text)
+    },
+    edit: {
+        type: input
+    },
+    search: true
+}, {
+    title: () => $gettext('Auto Cert'),
+    dataIndex: 'auto_cert',
+    customRender: (args: customRender) => {
+        const template: any = []
+        const {text, column} = args
+        if (text === true || text > 0) {
+            template.push(<Badge status="success"/>)
+            template.push($gettext('Enabled'))
+        } else {
+            template.push(<Badge status="warning"/>)
+            template.push($gettext('Disabled'))
+        }
+        return h('div', template)
+    },
+    sorter: true,
+    pithy: true
+}]
+
+function open() {
+    visible.value = true
+}
+
+function onSelectedRecord(r: any) {
+    record.value = r
+}
+
+function ok() {
+    props.directivesMap['ssl_certificate'][0]['params'] = record.value.ssl_certificate_path
+    props.directivesMap['ssl_certificate_key'][0]['params'] = record.value.ssl_certificate_key_path
+    visible.value = false
+}
+</script>
+
+<template>
+    <div>
+        <a-button @click="open">{{ $gettext('Change Certificate') }}</a-button>
+        <a-modal
+            :title="$gettext('Change Certificate')"
+            v-model:visible="visible"
+            :mask="false"
+            @ok="ok"
+        >
+            <std-table
+                :api="cert"
+                :pithy="true"
+                :columns="columns"
+                selectionType="radio"
+                @onSelectedRecord="onSelectedRecord"
+            />
+        </a-modal>
+    </div>
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 31 - 40
frontend/src/views/domain/cert/IssueCert.vue

@@ -4,6 +4,7 @@ import {computed, h, nextTick, onMounted, ref, VNode, watch} from 'vue'
 import {message} from 'ant-design-vue'
 import domain from '@/api/domain'
 import websocket from '@/lib/websocket'
+import Template from '@/views/template/Template.vue'
 
 const {$gettext, interpolate} = useGettext()
 
@@ -31,12 +32,6 @@ function job() {
         return
     }
 
-    if (server_name_more_than_one.value) {
-        message.error($gettext('server_name parameters more than one'))
-        issuing_cert.value = false
-        return
-    }
-
     const server_name = props.directivesMap['server_name'][0]
 
     if (!props.directivesMap['ssl_certificate']) {
@@ -62,8 +57,6 @@ function job() {
 function callback(ssl_certificate: string, ssl_certificate_key: string) {
     props.directivesMap['ssl_certificate'][0]['params'] = ssl_certificate
     props.directivesMap['ssl_certificate_key'][0]['params'] = ssl_certificate_key
-
-    emit('callback')
 }
 
 function change_auto_cert(r: boolean) {
@@ -102,10 +95,12 @@ const issue_cert = async (server_name: string, callback: Function) => {
 
     log($gettext('Getting the certificate, please wait...'))
 
-    const ws = websocket('/api/cert/issue/' + server_name, false)
+    const ws = websocket('/api/cert/issue', false)
 
     ws.onopen = () => {
-        ws.send('go')
+        ws.send(JSON.stringify({
+            server_name: server_name.trim().split(' ')
+        }))
     }
 
     ws.onmessage = m => {
@@ -132,13 +127,8 @@ const issue_cert = async (server_name: string, callback: Function) => {
     }
 }
 
-const server_name_more_than_one = computed(() => {
-    return props.directivesMap['server_name'] && (props.directivesMap['server_name'].length > 1 ||
-        props.directivesMap['server_name'][0].params.trim().indexOf(' ') > 0)
-})
-
 const no_server_name = computed(() => {
-    return props.directivesMap['server_name'].length === 0
+    return props.directivesMap['server_name']?.length === 0
 })
 
 const name = computed(() => {
@@ -154,11 +144,6 @@ const enabled = computed({
     }
 })
 
-watch(server_name_more_than_one, () => {
-    emit('update:enabled', false)
-    onchange(false)
-})
-
 watch(no_server_name, () => {
     emit('update:enabled', false)
     onchange(false)
@@ -166,7 +151,7 @@ watch(no_server_name, () => {
 
 const progressStrokeColor = {
     from: '#108ee9',
-    to: '#87d068',
+    to: '#87d068'
 }
 
 const progressPercent = ref(0)
@@ -180,6 +165,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"
@@ -191,16 +177,16 @@ const modalClosable = ref(false)
         </div>
 
     </a-modal>
-    <div>
+    <div class="issue-cert">
         <a-form-item :label="$gettext('Encrypt website with Let\'s Encrypt')">
             <a-switch
                 :loading="issuing_cert"
                 v-model:checked="enabled"
                 @change="onchange"
-                :disabled="no_server_name||server_name_more_than_one"
+                :disabled="no_server_name"
             />
             <a-alert
-                v-if="no_server_name||server_name_more_than_one"
+                v-if="no_server_name"
                 :message="$gettext('Warning')"
                 type="warning"
                 show-icon
@@ -209,24 +195,25 @@ const modalClosable = ref(false)
                     <span v-if="no_server_name" v-translate>
                         server_name parameter is required
                     </span>
-                    <span v-if="server_name_more_than_one" v-translate>
-                        server_name parameters more than one
-                    </span>
                 </template>
             </a-alert>
         </a-form-item>
-        <p v-translate>
-            Note: The server_name in the current configuration must be the domain name
-            you need to get the certificate.
-        </p>
-        <p v-translate>
-            The certificate for the domain will be checked every hour,
-            and will be renewed if it has been more than 1 month since it was last issued.
-        </p>
-        <p v-translate>
-            Make sure you have configured a reverse proxy for .well-known
-            directory to HTTPChallengePort (default: 9180) before getting the certificate.
-        </p>
+        <a-alert type="info" closable :message="$gettext('Note')">
+            <template #description>
+                <p v-translate>
+                    The server_name in the current configuration must be the domain name
+                    you need to get the certificate.
+                </p>
+                <p v-translate>
+                    The certificate for the domain will be checked every hour,
+                    and will be renewed if it has been more than 1 month since it was last issued.
+                </p>
+                <p v-translate>
+                    Make sure you have configured a reverse proxy for .well-known
+                    directory to HTTPChallengePort (default: 9180) before getting the certificate.
+                </p>
+            </template>
+        </a-alert>
     </div>
 </template>
 
@@ -247,6 +234,10 @@ const modalClosable = ref(false)
 </style>
 
 <style lang="less" scoped>
+.issue-cert {
+    margin: 15px 0;
+}
+
 .switch-wrapper {
     position: relative;
 

+ 106 - 0
frontend/src/views/domain/ngx_conf/ConfigTemplate.vue

@@ -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)">{{ $gettext('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>

+ 3 - 3
frontend/src/views/domain/ngx_conf/LocationEditor.vue

@@ -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>

+ 8 - 1
frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue

@@ -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>
 

+ 9 - 7
frontend/src/views/domain/ngx_conf/directive/DirectiveAdd.vue

@@ -24,8 +24,8 @@ function add() {
 
 function save() {
     adding.value = false
-    if (mode.value === If) {
-        directive.directive = If
+    if (mode.value === 'multi-line') {
+        directive.directive = ''
     }
 
     if (props.idx) {
@@ -42,19 +42,19 @@ function save() {
     <div>
         <div class="add-directive-temp" v-if="adding">
             <a-form-item>
-                <a-select v-model:value="mode" default-value="default" style="width: 150px">
+                <a-select v-model:value="mode" default-value="default" style="width: 180px">
                     <a-select-option value="default">
                         {{ $gettext('Single Directive') }}
                     </a-select-option>
-                    <a-select-option value="if">
-                        if
+                    <a-select-option value="multi-line">
+                        {{ $gettext('Multi-line Directive') }}
                     </a-select-option>
                 </a-select>
             </a-form-item>
             <a-form-item>
 
                 <div class="input-wrapper">
-                    <code-editor v-if="mode===If" default-height="100px" style="width: 100%;"
+                    <code-editor v-if="mode==='multi-line'" default-height="100px" style="width: 100%;"
                                  v-model:content="directive.params"/>
                     <a-input-group v-else compact>
                         <a-input style="width: 30%" :placeholder="$gettext('Directive')"
@@ -73,7 +73,9 @@ function save() {
         </div>
         <a-button block v-if="!adding" @click="add">{{ $gettext('Add Directive Below') }}</a-button>
         <a-button type="primary" v-else block @click="save"
-                  :disabled="!directive.directive||!directive.params">{{ $gettext('Save Directive') }}
+                  :disabled="(mode==='default'&&(!directive.directive||!directive.params))
+                  ||(!directive.params&&mode==='multi-line')">
+            {{ $gettext('Save Directive') }}
         </a-button>
     </div>
 </template>

+ 10 - 82
frontend/src/views/domain/ngx_conf/directive/DirectiveEditor.vue

@@ -1,17 +1,13 @@
 <script setup lang="ts">
-import CodeEditor from '@/components/CodeEditor'
-import {If} from '@/views/domain/ngx_conf'
 import DirectiveAdd from '@/views/domain/ngx_conf/directive/DirectiveAdd'
 import {useGettext} from 'vue3-gettext'
 import {reactive, ref} from 'vue'
-import {DeleteOutlined, HolderOutlined} from '@ant-design/icons-vue'
 import draggable from 'vuedraggable'
+import DirectiveEditorItem from '@/views/domain/ngx_conf/directive/DirectiveEditorItem.vue'
 
 const {$gettext} = useGettext()
 
-const props = defineProps<{
-    ngx_directives: any[]
-}>()
+const props = defineProps(['ngx_directives', 'readonly'])
 
 const adding = ref(false)
 
@@ -19,20 +15,6 @@ let directive = reactive({})
 
 const current_idx = ref(-1)
 
-function add() {
-    adding.value = true
-    directive = reactive({})
-}
-
-function save() {
-    adding.value = false
-    props.ngx_directives.push(directive)
-}
-
-function remove(index: number) {
-    props.ngx_directives.splice(index, 1)
-}
-
 function onSave(idx: number) {
     setTimeout(() => {
         current_idx.value = idx + 1
@@ -48,75 +30,21 @@ function onSave(idx: number) {
         item-key="name"
         class="list-group"
         ghost-class="ghost"
-        handle=".ant-input-group-addon"
+        handle=".anticon-holder"
     >
         <template #item="{ element: directive, index }">
-            <a-form-item @click="current_idx=index">
-
-                <div class="input-wrapper">
-                    <code-editor v-if="directive.directive === If" v-model:content="directive.params"
-                                 defaultHeight="100px" style="width: 100%;"/>
-
-                    <a-input v-else
-                             v-model:value="directive.params" @click="current_idx=index" @blur="current_idx=-1">
-                        <template #addonBefore>
-                            <HolderOutlined/>
-                            {{ directive.directive }}
-                        </template>
-                    </a-input>
-
-                    <a-popconfirm @confirm="remove(index)"
-                                  :title="$gettext('Are you sure you want to remove this directive?')"
-                                  :ok-text="$gettext('Yes')"
-                                  :cancel-text="$gettext('No')">
-                        <a-button>
-                            <template #icon>
-                                <DeleteOutlined style="font-size: 14px;"/>
-                            </template>
-                        </a-button>
-                    </a-popconfirm>
-                </div>
-                <transition name="slide">
-                    <div v-if="current_idx===index" class="directive-editor-extra">
-                        <div class="extra-content">
-                            <a-form layout="vertical">
-                                <a-form-item :label="$gettext('Comments')">
-                                    <a-textarea v-model:value="directive.comments"/>
-                                </a-form-item>
-                            </a-form>
-                        </div>
-                    </div>
-                </transition>
-            </a-form-item>
+            <directive-editor-item @click="current_idx=index"
+                                   :directive="directive"
+                                   :current_idx="current_idx" :index="index"
+                                   :ngx_directives="ngx_directives"
+                                   :readonly="readonly"
+            />
         </template>
     </draggable>
 
-    <directive-add :ngx_directives="props.ngx_directives"/>
+    <directive-add v-if="!readonly" :ngx_directives="ngx_directives"/>
 </template>
 
 <style lang="less" scoped>
-.directive-editor-extra {
-    background-color: #fafafa;
-    padding: 10px 20px;
-    margin-bottom: 10px;
-}
-
-.slide-enter-active, .slide-leave-active {
-    transition: max-height .2s ease;
-    overflow: hidden;
-}
-
-.slide-enter-from, .slide-leave-to {
-    max-height: 0;
-}
-
-.slide-enter-to, .slide-leave-from {
-    max-height: 600px;
-}
 
-.input-wrapper {
-    display: flex;
-    gap: 10px;
-    align-items: center;
-}
 </style>

+ 138 - 0
frontend/src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue

@@ -0,0 +1,138 @@
+<script setup lang="ts">
+import CodeEditor from '@/components/CodeEditor'
+import {DeleteOutlined, HolderOutlined} from '@ant-design/icons-vue'
+import {If} from '@/views/domain/ngx_conf'
+
+import {useGettext} from 'vue3-gettext'
+import {onMounted, ref, watch} from 'vue'
+import config from '@/api/config'
+import {message} from 'ant-design-vue'
+
+const {$gettext, interpolate} = useGettext()
+
+const props = defineProps(['directive', 'current_idx', 'index', 'ngx_directives', 'readonly'])
+
+function remove(index: number) {
+    props.ngx_directives.splice(index, 1)
+}
+
+const content = ref('')
+
+function init() {
+    if (props.directive.directive === 'include')
+        config.get(props.directive.params).then(r => {
+            content.value = r.config
+        })
+}
+
+onMounted(init)
+
+watch(props, init)
+
+function save() {
+    config.save(props.directive.params, {content: content.value}).then(r => {
+        content.value = r.config
+        message.success($gettext('Saved successfully'))
+    }).catch(r => {
+        message.error(interpolate($gettext('Save error %{msg}'), {msg: r.message ?? ''}))
+    })
+}
+</script>
+
+<template>
+    <div class="dir-editor-item">
+        <div class="input-wrapper">
+            <div class="code-editor-wrapper" v-if="directive.directive === ''">
+                <HolderOutlined style="padding: 5px"/>
+                <code-editor v-model:content="directive.params"
+                             defaultHeight="100px" style="width: 100%;"/>
+            </div>
+
+            <a-input v-else
+                     v-model:value="directive.params" @click="current_idx=index">
+                <template #addonBefore>
+                    <HolderOutlined/>
+                    {{ directive.directive }}
+                </template>
+            </a-input>
+
+            <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')">
+                <a-button>
+                    <template #icon>
+                        <DeleteOutlined style="font-size: 14px;"/>
+                    </template>
+                </a-button>
+            </a-popconfirm>
+        </div>
+        <transition name="slide">
+            <div v-if="current_idx===index" class="directive-editor-extra">
+                <div class="extra-content">
+                    <a-form layout="vertical">
+                        <a-form-item :label="$gettext('Comments')">
+                            <a-textarea v-model:value="directive.comments"/>
+                        </a-form-item>
+                        <a-form-item :label="$gettext('Content')" v-if="directive.directive==='include'">
+                            <code-editor v-model:content="content"
+                                         defaultHeight="200px" style="width: 100%;"/>
+                            <div class="save-btn">
+                                <a-button @click="save">{{ $gettext('Save') }}</a-button>
+                            </div>
+                        </a-form-item>
+                    </a-form>
+                </div>
+            </div>
+        </transition>
+    </div>
+
+</template>
+
+<style lang="less" scoped>
+.dir-editor-item {
+    margin: 15px 0;
+}
+
+.code-editor-wrapper {
+    display: flex;
+    width: 100%;
+    align-items: center;
+}
+
+.anticon-holder {
+    cursor: grab;
+}
+
+.directive-editor-extra {
+    background-color: #fafafa;
+    padding: 10px 20px;
+    margin-bottom: 10px;
+
+    .save-btn {
+        display: flex;
+        justify-content: flex-end;
+        margin-top: 15px;
+    }
+}
+
+.slide-enter-active, .slide-leave-active {
+    transition: max-height .2s ease;
+    overflow: hidden;
+}
+
+.slide-enter-from, .slide-leave-to {
+    max-height: 0;
+}
+
+.slide-enter-to, .slide-leave-from {
+    max-height: 600px;
+}
+
+.input-wrapper {
+    display: flex;
+    gap: 10px;
+    align-items: center;
+}
+</style>

+ 2 - 2
frontend/src/views/nginx_log/NginxLog.vue

@@ -68,8 +68,8 @@ function init() {
     nginx_log.page(0, {
         conf_name: (route.query.conf_name as string),
         type: logType(),
-        server_idx: 0,
-        directive_idx: 0
+        server_idx: parseInt(route.query.server_idx as string),
+        directive_idx: parseInt(route.query.directive_idx as string)
     }).then(r => {
         page.value = r.page - 1
         addLog(r.content)

+ 101 - 0
frontend/src/views/preference/Preference.vue

@@ -0,0 +1,101 @@
+<script setup lang="ts">
+import {useGettext} from 'vue3-gettext'
+import {reactive, ref} from 'vue'
+import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
+import {useSettingsStore} from '@/pinia'
+import {dark_mode} from '@/lib/theme'
+import settings from '@/api/settings'
+import {message} from 'ant-design-vue'
+
+const {$gettext} = useGettext()
+
+const settingsStore = useSettingsStore()
+const theme = ref('auto')
+const data = ref({
+    server: {
+        http_port: 9000,
+        run_mode: 'debug',
+        jwt_secret: '',
+        start_cmd: '',
+        email: '',
+        http_challenge_port: 9180
+    },
+    nginx_log: {
+        access_log_path: '',
+        error_log_path: ''
+    }
+})
+
+settings.get().then(r => {
+    data.value = r
+})
+
+function save() {
+    settingsStore.set_theme(theme.value)
+    settingsStore.set_preference_theme(theme.value)
+    dark_mode(theme.value === 'dark')
+    settings.save(data.value).then(r => {
+        data.value = r
+        message.success($gettext('Save successfully'))
+    }).catch(e => {
+        message.error(e?.message ?? $gettext('Server error'))
+    })
+}
+</script>
+
+<template>
+    <a-card :title="$gettext('Preference')">
+        <div class="preference-container">
+            <a-form layout="vertical">
+                <a-form-item :label="$gettext('HTTP Port')">
+                    <p>{{ data.server.http_port }}</p>
+                </a-form-item>
+                <a-form-item :label="$gettext('Run Mode')">
+                    <p>{{ data.server.run_mode }}</p>
+                </a-form-item>
+                <a-form-item :label="$gettext('Jwt Secret')">
+                    <p>{{ data.server.jwt_secret }}</p>
+                </a-form-item>
+                <a-form-item :label="$gettext('Terminal Start Command')">
+                    <p>{{ data.server.start_cmd }}</p>
+                </a-form-item>
+                <a-form-item :label="$gettext('HTTP Challenge Port')">
+                    <a-input-number v-model:value="data.server.http_challenge_port"/>
+                </a-form-item>
+                <a-form-item :label="$gettext('Theme')">
+                    <a-select v-model:value="theme">
+                        <a-select-option value="auto">
+                            {{ $gettext('Auto') }}
+                        </a-select-option>
+                        <a-select-option value="light">
+                            {{ $gettext('Light') }}
+                        </a-select-option>
+                        <a-select-option value="dark">
+                            {{ $gettext('Dark') }}
+                        </a-select-option>
+                    </a-select>
+                </a-form-item>
+                <a-form-item :label="$gettext('Nginx Access Log Path')">
+                    <a-input v-model:value="data.nginx_log.access_log_path"/>
+                </a-form-item>
+                <a-form-item :label="$gettext('Nginx Error Log Path')">
+                    <a-input v-model:value="data.nginx_log.error_log_path"/>
+                </a-form-item>
+            </a-form>
+        </div>
+    </a-card>
+    <footer-tool-bar>
+        <a-button type="primary" @click="save">
+            {{ $gettext('Save') }}
+        </a-button>
+    </footer-tool-bar>
+</template>
+
+<style lang="less" scoped>
+.preference-container {
+    width: 100%;
+    max-width: 600px;
+    margin: 0 auto;
+    padding: 0 10px;
+}
+</style>

+ 11 - 0
frontend/src/views/template/Template.vue

@@ -0,0 +1,11 @@
+<script setup lang="ts">
+
+</script>
+
+<template>
+
+</template>
+
+<style lang="less" scoped>
+
+</style>

+ 1 - 1
frontend/version.json

@@ -1 +1 @@
-{"version":"1.6.8","build_id":57,"total_build":127}
+{"version":"1.7.0","build_id":0,"total_build":0}

+ 149 - 149
frontend/yarn.lock

@@ -361,20 +361,120 @@
   resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz#75b4c27948c81e88ccd3a8902047bcd797f38d32"
   integrity sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==
 
-"@esbuild/android-arm@0.15.13":
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.13.tgz#ce11237a13ee76d5eae3908e47ba4ddd380af86a"
-  integrity sha512-RY2fVI8O0iFUNvZirXaQ1vMvK0xhCcl0gqRj74Z6yEiO1zAUa7hbsdwZM1kzqbxHK7LFyMizipfXT3JME+12Hw==
+"@esbuild/android-arm64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.16.13.tgz#1fc9bfbff0bac558008b2ad7242db1c8024d8cfd"
+  integrity sha512-r4xetsd1ez1NF9/9R2f9Q6AlxqiZLwUqo7ICOcvEVwopVkXUcspIjEbJk0EVTgT6Cp5+ymzGPT6YNV0ievx4yA==
+
+"@esbuild/android-arm@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.16.13.tgz#df3317286eed68c727daf39c2d585625f9c2f170"
+  integrity sha512-JmtqThupn9Yf+FzANE+GG73ASUkssnPwOsndUElhp23685QzRK+MO1UompOlBaXV9D5FTuYcPnw7p4mCq2YbZQ==
+
+"@esbuild/android-x64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.16.13.tgz#c34826c4bdc57c60cbfb8d5bbd2306a89225626a"
+  integrity sha512-hKt1bFht/Vtp0xJ0ZVzFMnPy1y1ycmM3KNnp3zsyZfQmw7nhs2WLO4vxdR5YG+6RsHKCb2zbZ3VwlC0Tij0qyA==
+
+"@esbuild/darwin-arm64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.13.tgz#0b80c8580c262ccfb1203053201cf19c6f7b4cdb"
+  integrity sha512-ogrVuNi2URocrr3Ps20f075EMm9V7IeenOi9FRj4qdbT6mQlwLuP4l90PW2iBrKERx0oRkcZprEUNsz/3xd7ww==
+
+"@esbuild/darwin-x64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.16.13.tgz#f1a6c9ea67d4eaaf4944e1cbceb800eabc6e7e74"
+  integrity sha512-Agajik9SBGiKD7FPXE+ExW6x3MgA/dUdpZnXa9y1tyfE4lKQx+eQiknSdrBnWPeqa9wL0AOvkhghmYhpVkyqkA==
+
+"@esbuild/freebsd-arm64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.13.tgz#d1a45ac5c4a1be566c4eefbadbe5a967288ad338"
+  integrity sha512-KxMO3/XihBcHM+xQUM6nQZO1SgQuOsd1DCnKF1a4SIf/i5VD45vrqN3k8ePgFrEbMi7m5JeGmvNqwJXinF0a4Q==
+
+"@esbuild/freebsd-x64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.13.tgz#ec64a31cabb08343bb4520a221324b40509dffc8"
+  integrity sha512-Ez15oqV1vwvZ30cVLeBW14BsWq/fdWNQGMOxxqaSJVQVLqHhvgfQ7gxGDiN9tpJdeQhqJO+Q0r02/Tce5+USNg==
+
+"@esbuild/linux-arm64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.16.13.tgz#e8db3c3751b32ecf801751424eae43f6863a2ee7"
+  integrity sha512-qi5n7KwcGViyJeZeQnu8fB6dC3Mlm5PGaqSv2HhQDDx/MPvVfQGNMcv7zcBL4qk3FkuWhGVwXkjQ76x7R0PWlA==
+
+"@esbuild/linux-arm@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.16.13.tgz#ac0c8e9f3db8d418f588ae250e9c66ffdcf3cd82"
+  integrity sha512-18dLd2L3mda+iFj6sswyBMSh2UwniamD9M4DwPv8VM+9apRFlQ5IGKxBdumnTuOI4NvwwAernmUseWhYQ9k+rg==
+
+"@esbuild/linux-ia32@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.16.13.tgz#41ee9bd3b7161ab681fab6cb3990a3f5c08a9940"
+  integrity sha512-2489Xad9sr+6GD7nB913fUqpCsSwVwgskkQTq4Or2mZntSPYPebyJm8l1YruHo7oqYMTGV6RiwGE4gRo3H+EPQ==
 
 "@esbuild/linux-loong64@0.14.53":
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.53.tgz#251b4cd6760fadb4d68a05815e6dc5e432d69cd6"
   integrity sha512-W2dAL6Bnyn4xa/QRSU3ilIK4EzD5wgYXKXJiS1HDF5vU3675qc2bvFyLwbUcdmssDveyndy7FbitrCoiV/eMLg==
 
-"@esbuild/linux-loong64@0.15.13":
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.13.tgz#64e8825bf0ce769dac94ee39d92ebe6272020dfc"
-  integrity sha512-+BoyIm4I8uJmH/QDIH0fu7MG0AEx9OXEDXnqptXCwKOlOqZiS4iraH1Nr7/ObLMokW3sOCeBNyD68ATcV9b9Ag==
+"@esbuild/linux-loong64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.16.13.tgz#e4a832708e0b47078b91413edcfdb6af88c854a3"
+  integrity sha512-x8KplRu9Y43Px8I9YS+sPBwQ+fw44Mvp2BPVADopKDWz+h3fcj1BvRU58kxb89WObmwKX9sWdtYzepL4Fmx03A==
+
+"@esbuild/linux-mips64el@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.13.tgz#30d8571b71e0b8bf25fc5ef11422221ed23cdacc"
+  integrity sha512-qhhdWph9FLwD9rVVC/nUf7k2U4NZIA6/mGx0B7+O6PFV0GjmPA2E3zDQ4NUjq9P26E0DeAZy9akH9dYcUBRU7A==
+
+"@esbuild/linux-ppc64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.13.tgz#32a3855d4b79ba1d2b63dab592cb9f0d4a9ba485"
+  integrity sha512-cVWAPKsrRVxI1jCeJHnYSbE3BrEU+pZTZK2gfao9HRxuc+3m4+RLfs3EVEpGLmMKEcWfVCB9wZ3yNxnknutGKQ==
+
+"@esbuild/linux-riscv64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.13.tgz#6139202858da8202724d7079102614c269524f34"
+  integrity sha512-Agb7dbRyZWnmPn5Vvf0eyqaEUqSsaIUwwyInu2EoFTaIDRp093QU2M5alUyOooMLkRbD1WvqQNwx08Z/g+SAcQ==
+
+"@esbuild/linux-s390x@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.16.13.tgz#df3550a51e4155cde31486e01d8121f078e959ae"
+  integrity sha512-AqRBIrc/+kl08ahliNG+EyU+j41wIzQfwBTKpi80cCDiYvYFPuXjvzZsD9muiu58Isj0RVni9VgC4xK/AnSW4g==
+
+"@esbuild/linux-x64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.16.13.tgz#deb7951829ea5930e0d88440aeb5df0756ebb2d0"
+  integrity sha512-S4wn2BimuhPcoArRtVrdHUKIymCCZcYAXQE47kUiX4yrUrEX2/ifn5eKNbZ5c1jJKUlh1gC2ESIN+iw3wQax3g==
+
+"@esbuild/netbsd-x64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.13.tgz#8cba08074263862138cc5c63ad7f9640fe3faa69"
+  integrity sha512-2c8JWgfUMlQHTdaR5X3xNMwqOyad8kgeCupuVkdm3QkUOzGREjlTETQsK6oHifocYzDCo9FeKcUwsK356SdR+g==
+
+"@esbuild/openbsd-x64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.13.tgz#4ae19ac63c665424d248ba5c577618dd7bbcebd5"
+  integrity sha512-Bwh+PmKD/LK+xBjqIpnYnKYj0fIyQJ0YpRxsn0F+WfzvQ2OA+GKDlf8AHosiCns26Q4Dje388jQVwfOBZ1GaFw==
+
+"@esbuild/sunos-x64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.16.13.tgz#592caacab6b2c7669cd869b51f66dc354da03fc2"
+  integrity sha512-8wwk6f9XGnhrF94/DBdFM4Xm1JeCyGTCj67r516VS9yvBVQf3Rar54L+XPVDs/oZOokwH+XsktrgkuTMAmjntg==
+
+"@esbuild/win32-arm64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.16.13.tgz#965ebbe889e4221962250c55facaa1e48130c162"
+  integrity sha512-Jmwbp/5ArLCiRAHC33ODfcrlIcbP/exXkOEUVkADNJC4e/so2jm+i8IQFvVX/lA2GWvK3GdgcN0VFfp9YITAbg==
+
+"@esbuild/win32-ia32@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.16.13.tgz#1b04965bcf340ba4879b452ac32df63216d4c87e"
+  integrity sha512-AX6WjntGjhJHzrPSVvjMD7grxt41koHfAOx6lxLorrpDwwIKKPaGDASPZgvFIZHTbwhOtILW6vAXxYPDsKpDJA==
+
+"@esbuild/win32-x64@0.16.13":
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.16.13.tgz#0b0989cf0e7887cb0f3124e705cee68a694b96dd"
+  integrity sha512-A+U4gM6OOkPS03UgVU08GTpAAAxPsP/8Z4FmneGo4TaVSD99bK9gVJXlqUEPMO/htFXEAht2O6pX4ErtLY5tVg==
 
 "@jridgewell/gen-mapping@^0.1.0":
   version "0.1.1"
@@ -1442,201 +1542,101 @@ esbuild-android-64@0.14.53:
   resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.53.tgz#259bc3ef1399a3cad8f4f67c40ee20779c4de675"
   integrity sha512-fIL93sOTnEU+NrTAVMIKiAw0YH22HWCAgg4N4Z6zov2t0kY9RAJ50zY9ZMCQ+RT6bnOfDt8gCTnt/RaSNA2yRA==
 
-esbuild-android-64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.13.tgz#5f25864055dbd62e250f360b38b4c382224063af"
-  integrity sha512-yRorukXBlokwTip+Sy4MYskLhJsO0Kn0/Fj43s1krVblfwP+hMD37a4Wmg139GEsMLl+vh8WXp2mq/cTA9J97g==
-
 esbuild-android-arm64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.53.tgz#2158253d4e8f9fdd2a081bbb4f73b8806178841e"
   integrity sha512-PC7KaF1v0h/nWpvlU1UMN7dzB54cBH8qSsm7S9mkwFA1BXpaEOufCg8hdoEI1jep0KeO/rjZVWrsH8+q28T77A==
 
-esbuild-android-arm64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.13.tgz#d8820f999314efbe8e0f050653a99ff2da632b0f"
-  integrity sha512-TKzyymLD6PiVeyYa4c5wdPw87BeAiTXNtK6amWUcXZxkV51gOk5u5qzmDaYSwiWeecSNHamFsaFjLoi32QR5/w==
-
 esbuild-darwin-64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.53.tgz#b4681831fd8f8d06feb5048acbe90d742074cc2a"
   integrity sha512-gE7P5wlnkX4d4PKvLBUgmhZXvL7lzGRLri17/+CmmCzfncIgq8lOBvxGMiQ4xazplhxq+72TEohyFMZLFxuWvg==
 
-esbuild-darwin-64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.13.tgz#99ae7fdaa43947b06cd9d1a1c3c2c9f245d81fd0"
-  integrity sha512-WAx7c2DaOS6CrRcoYCgXgkXDliLnFv3pQLV6GeW1YcGEZq2Gnl8s9Pg7ahValZkpOa0iE/ojRVQ87sbUhF1Cbg==
-
 esbuild-darwin-arm64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.53.tgz#d267d957852d121b261b3f76ead86e5b5463acc9"
   integrity sha512-otJwDU3hnI15Q98PX4MJbknSZ/WSR1I45il7gcxcECXzfN4Mrpft5hBDHXNRnCh+5858uPXBXA1Vaz2jVWLaIA==
 
-esbuild-darwin-arm64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.13.tgz#bafa1814354ad1a47adcad73de416130ef7f55e3"
-  integrity sha512-U6jFsPfSSxC3V1CLiQqwvDuj3GGrtQNB3P3nNC3+q99EKf94UGpsG9l4CQ83zBs1NHrk1rtCSYT0+KfK5LsD8A==
-
 esbuild-freebsd-64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.53.tgz#aca2af6d72b537fe66a38eb8f374fb66d4c98ca0"
   integrity sha512-WkdJa8iyrGHyKiPF4lk0MiOF87Q2SkE+i+8D4Cazq3/iqmGPJ6u49je300MFi5I2eUsQCkaOWhpCVQMTKGww2w==
 
-esbuild-freebsd-64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.13.tgz#84ef85535c5cc38b627d1c5115623b088d1de161"
-  integrity sha512-whItJgDiOXaDG/idy75qqevIpZjnReZkMGCgQaBWZuKHoElDJC1rh7MpoUgupMcdfOd+PgdEwNQW9DAE6i8wyA==
-
 esbuild-freebsd-arm64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.53.tgz#76282e19312d914c34343c8a7da6cc5f051580b9"
   integrity sha512-9T7WwCuV30NAx0SyQpw8edbKvbKELnnm1FHg7gbSYaatH+c8WJW10g/OdM7JYnv7qkimw2ZTtSA+NokOLd2ydQ==
 
-esbuild-freebsd-arm64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.13.tgz#033f21de434ec8e0c478054b119af8056763c2d8"
-  integrity sha512-6pCSWt8mLUbPtygv7cufV0sZLeylaMwS5Fznj6Rsx9G2AJJsAjQ9ifA+0rQEIg7DwJmi9it+WjzNTEAzzdoM3Q==
-
 esbuild-linux-32@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.53.tgz#1045d34cf7c5faaf2af3b29cc1573b06580c37e5"
   integrity sha512-VGanLBg5en2LfGDgLEUxQko2lqsOS7MTEWUi8x91YmsHNyzJVT/WApbFFx3MQGhkf+XdimVhpyo5/G0PBY91zg==
 
-esbuild-linux-32@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.13.tgz#54290ea8035cba0faf1791ce9ae6693005512535"
-  integrity sha512-VbZdWOEdrJiYApm2kkxoTOgsoCO1krBZ3quHdYk3g3ivWaMwNIVPIfEE0f0XQQ0u5pJtBsnk2/7OPiCFIPOe/w==
-
 esbuild-linux-64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz#ab3f2ee2ebb5a6930c72d9539cb34b428808cbe4"
   integrity sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==
 
-esbuild-linux-64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.13.tgz#4264249281ea388ead948614b57fb1ddf7779a2c"
-  integrity sha512-rXmnArVNio6yANSqDQlIO4WiP+Cv7+9EuAHNnag7rByAqFVuRusLbGi2697A5dFPNXoO//IiogVwi3AdcfPC6A==
-
 esbuild-linux-arm64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.53.tgz#1f5530412f6690949e78297122350488d3266cfe"
   integrity sha512-GDmWITT+PMsjCA6/lByYk7NyFssW4Q6in32iPkpjZ/ytSyH+xeEx8q7HG3AhWH6heemEYEWpTll/eui3jwlSnw==
 
-esbuild-linux-arm64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.13.tgz#9323c333924f97a02bdd2ae8912b36298acb312d"
-  integrity sha512-alEMGU4Z+d17U7KQQw2IV8tQycO6T+rOrgW8OS22Ua25x6kHxoG6Ngry6Aq6uranC+pNWNMB6aHFPh7aTQdORQ==
-
 esbuild-linux-arm@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz#a44ec9b5b42007ab6c0d65a224ccc6bbd97c54cf"
   integrity sha512-/u81NGAVZMopbmzd21Nu/wvnKQK3pT4CrvQ8BTje1STXcQAGnfyKgQlj3m0j2BzYbvQxSy+TMck4TNV2onvoPA==
 
-esbuild-linux-arm@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.13.tgz#b407f47b3ae721fe4e00e19e9f19289bef87a111"
-  integrity sha512-Ac6LpfmJO8WhCMQmO253xX2IU2B3wPDbl4IvR0hnqcPrdfCaUa2j/lLMGTjmQ4W5JsJIdHEdW12dG8lFS0MbxQ==
-
 esbuild-linux-mips64le@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.53.tgz#a4d0b6b17cfdeea4e41b0b085a5f73d99311be9f"
   integrity sha512-d6/XHIQW714gSSp6tOOX2UscedVobELvQlPMkInhx1NPz4ThZI9uNLQ4qQJHGBGKGfu+rtJsxM4NVHLhnNRdWQ==
 
-esbuild-linux-mips64le@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.13.tgz#bdf905aae5c0bcaa8f83567fe4c4c1bdc1f14447"
-  integrity sha512-47PgmyYEu+yN5rD/MbwS6DxP2FSGPo4Uxg5LwIdxTiyGC2XKwHhHyW7YYEDlSuXLQXEdTO7mYe8zQ74czP7W8A==
-
 esbuild-linux-ppc64le@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.53.tgz#8c331822c85465434e086e3e6065863770c38139"
   integrity sha512-ndnJmniKPCB52m+r6BtHHLAOXw+xBCWIxNnedbIpuREOcbSU/AlyM/2dA3BmUQhsHdb4w3amD5U2s91TJ3MzzA==
 
-esbuild-linux-ppc64le@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.13.tgz#2911eae1c90ff58a3bd3259cb557235df25aa3b4"
-  integrity sha512-z6n28h2+PC1Ayle9DjKoBRcx/4cxHoOa2e689e2aDJSaKug3jXcQw7mM+GLg+9ydYoNzj8QxNL8ihOv/OnezhA==
-
 esbuild-linux-riscv64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.53.tgz#36fd75543401304bea8a2d63bf8ea18aaa508e00"
   integrity sha512-yG2sVH+QSix6ct4lIzJj329iJF3MhloLE6/vKMQAAd26UVPVkhMFqFopY+9kCgYsdeWvXdPgmyOuKa48Y7+/EQ==
 
-esbuild-linux-riscv64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.13.tgz#1837c660be12b1d20d2a29c7189ea703f93e9265"
-  integrity sha512-+Lu4zuuXuQhgLUGyZloWCqTslcCAjMZH1k3Xc9MSEJEpEFdpsSU0sRDXAnk18FKOfEjhu4YMGaykx9xjtpA6ow==
-
 esbuild-linux-s390x@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.53.tgz#1622677ab6824123f48f75d3afc031cd41936129"
   integrity sha512-OCJlgdkB+XPYndHmw6uZT7jcYgzmx9K+28PVdOa/eLjdoYkeAFvH5hTwX4AXGLZLH09tpl4bVsEtvuyUldaNCg==
 
-esbuild-linux-s390x@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.13.tgz#d52880ece229d1bd10b2d936b792914ffb07c7fc"
-  integrity sha512-BMeXRljruf7J0TMxD5CIXS65y7puiZkAh+s4XFV9qy16SxOuMhxhVIXYLnbdfLrsYGFzx7U9mcdpFWkkvy/Uag==
-
 esbuild-netbsd-64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.53.tgz#e86d0efd0116658be335492ed12e66b26b4baf52"
   integrity sha512-gp2SB+Efc7MhMdWV2+pmIs/Ja/Mi5rjw+wlDmmbIn68VGXBleNgiEZG+eV2SRS0kJEUyHNedDtwRIMzaohWedQ==
 
-esbuild-netbsd-64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.13.tgz#de14da46f1d20352b43e15d97a80a8788275e6ed"
-  integrity sha512-EHj9QZOTel581JPj7UO3xYbltFTYnHy+SIqJVq6yd3KkCrsHRbapiPb0Lx3EOOtybBEE9EyqbmfW1NlSDsSzvQ==
-
 esbuild-openbsd-64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.53.tgz#9bcbbe6f86304872c6e91f64c8eb73fc29c3588b"
   integrity sha512-eKQ30ZWe+WTZmteDYg8S+YjHV5s4iTxeSGhJKJajFfQx9TLZJvsJX0/paqwP51GicOUruFpSUAs2NCc0a4ivQQ==
 
-esbuild-openbsd-64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.13.tgz#45e8a5fd74d92ad8f732c43582369c7990f5a0ac"
-  integrity sha512-nkuDlIjF/sfUhfx8SKq0+U+Fgx5K9JcPq1mUodnxI0x4kBdCv46rOGWbuJ6eof2n3wdoCLccOoJAbg9ba/bT2w==
-
 esbuild-sunos-64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.53.tgz#f7a872f7460bfb7b131f7188a95fbce3d1c577e8"
   integrity sha512-OWLpS7a2FrIRukQqcgQqR1XKn0jSJoOdT+RlhAxUoEQM/IpytS3FXzCJM6xjUYtpO5GMY0EdZJp+ur2pYdm39g==
 
-esbuild-sunos-64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.13.tgz#f646ac3da7aac521ee0fdbc192750c87da697806"
-  integrity sha512-jVeu2GfxZQ++6lRdY43CS0Tm/r4WuQQ0Pdsrxbw+aOrHQPHV0+LNOLnvbN28M7BSUGnJnHkHm2HozGgNGyeIRw==
-
 esbuild-windows-32@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.53.tgz#c5e3ca50e2d1439cc2c9fe4defa63bcd474ce709"
   integrity sha512-m14XyWQP5rwGW0tbEfp95U6A0wY0DYPInWBB7D69FAXUpBpBObRoGTKRv36lf2RWOdE4YO3TNvj37zhXjVL5xg==
 
-esbuild-windows-32@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.13.tgz#fb4fe77c7591418880b3c9b5900adc4c094f2401"
-  integrity sha512-XoF2iBf0wnqo16SDq+aDGi/+QbaLFpkiRarPVssMh9KYbFNCqPLlGAWwDvxEVz+ywX6Si37J2AKm+AXq1kC0JA==
-
 esbuild-windows-64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.53.tgz#ec2ab4a60c5215f092ffe1eab6d01319e88238af"
   integrity sha512-s9skQFF0I7zqnQ2K8S1xdLSfZFsPLuOGmSx57h2btSEswv0N0YodYvqLcJMrNMXh6EynOmWD7rz+0rWWbFpIHQ==
 
-esbuild-windows-64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.13.tgz#1fca8c654392c0c31bdaaed168becfea80e20660"
-  integrity sha512-Et6htEfGycjDrtqb2ng6nT+baesZPYQIW+HUEHK4D1ncggNrDNk3yoboYQ5KtiVrw/JaDMNttz8rrPubV/fvPQ==
-
 esbuild-windows-arm64@0.14.53:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.53.tgz#f71d403806bdf9f4a1f9d097db9aec949bd675c8"
   integrity sha512-E+5Gvb+ZWts+00T9II6wp2L3KG2r3iGxByqd/a1RmLmYWVsSVUjkvIxZuJ3hYTIbhLkH5PRwpldGTKYqVz0nzQ==
 
-esbuild-windows-arm64@0.15.13:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.13.tgz#4ffd01b6b2888603f1584a2fe96b1f6a6f2b3dd8"
-  integrity sha512-3bv7tqntThQC9SWLRouMDmZnlOukBhOCTlkzNqzGCmrkCJI7io5LLjwJBOVY6kOUlIvdxbooNZwjtBvj+7uuVg==
-
 esbuild@^0.14.47:
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.53.tgz#20b1007f686e8584f2a01a1bec5a37aac9498ce4"
@@ -1664,33 +1664,33 @@ esbuild@^0.14.47:
     esbuild-windows-64 "0.14.53"
     esbuild-windows-arm64 "0.14.53"
 
-esbuild@^0.15.9:
-  version "0.15.13"
-  resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.13.tgz#7293480038feb2bafa91d3f6a20edab3ba6c108a"
-  integrity sha512-Cu3SC84oyzzhrK/YyN4iEVy2jZu5t2fz66HEOShHURcjSkOSAVL8C/gfUT+lDJxkVHpg8GZ10DD0rMHRPqMFaQ==
+esbuild@^0.16.3:
+  version "0.16.13"
+  resolved "https://registry.npmmirror.com/esbuild/-/esbuild-0.16.13.tgz#83cd347c28221268bbfa0425db532d7d05f85b48"
+  integrity sha512-oYwFdSEIoKM1oYzyem1osgKJAvg5447XF+05ava21fOtilyb2HeQQh26/74K4WeAk5dZmj/Mx10zUqUnI14jhA==
   optionalDependencies:
-    "@esbuild/android-arm" "0.15.13"
-    "@esbuild/linux-loong64" "0.15.13"
-    esbuild-android-64 "0.15.13"
-    esbuild-android-arm64 "0.15.13"
-    esbuild-darwin-64 "0.15.13"
-    esbuild-darwin-arm64 "0.15.13"
-    esbuild-freebsd-64 "0.15.13"
-    esbuild-freebsd-arm64 "0.15.13"
-    esbuild-linux-32 "0.15.13"
-    esbuild-linux-64 "0.15.13"
-    esbuild-linux-arm "0.15.13"
-    esbuild-linux-arm64 "0.15.13"
-    esbuild-linux-mips64le "0.15.13"
-    esbuild-linux-ppc64le "0.15.13"
-    esbuild-linux-riscv64 "0.15.13"
-    esbuild-linux-s390x "0.15.13"
-    esbuild-netbsd-64 "0.15.13"
-    esbuild-openbsd-64 "0.15.13"
-    esbuild-sunos-64 "0.15.13"
-    esbuild-windows-32 "0.15.13"
-    esbuild-windows-64 "0.15.13"
-    esbuild-windows-arm64 "0.15.13"
+    "@esbuild/android-arm" "0.16.13"
+    "@esbuild/android-arm64" "0.16.13"
+    "@esbuild/android-x64" "0.16.13"
+    "@esbuild/darwin-arm64" "0.16.13"
+    "@esbuild/darwin-x64" "0.16.13"
+    "@esbuild/freebsd-arm64" "0.16.13"
+    "@esbuild/freebsd-x64" "0.16.13"
+    "@esbuild/linux-arm" "0.16.13"
+    "@esbuild/linux-arm64" "0.16.13"
+    "@esbuild/linux-ia32" "0.16.13"
+    "@esbuild/linux-loong64" "0.16.13"
+    "@esbuild/linux-mips64el" "0.16.13"
+    "@esbuild/linux-ppc64" "0.16.13"
+    "@esbuild/linux-riscv64" "0.16.13"
+    "@esbuild/linux-s390x" "0.16.13"
+    "@esbuild/linux-x64" "0.16.13"
+    "@esbuild/netbsd-x64" "0.16.13"
+    "@esbuild/openbsd-x64" "0.16.13"
+    "@esbuild/sunos-x64" "0.16.13"
+    "@esbuild/win32-arm64" "0.16.13"
+    "@esbuild/win32-ia32" "0.16.13"
+    "@esbuild/win32-x64" "0.16.13"
 
 escalade@^3.1.1:
   version "3.1.1"
@@ -2553,10 +2553,10 @@ postcss@^8.1.10, postcss@^8.2.9, postcss@^8.4.14:
     picocolors "^1.0.0"
     source-map-js "^1.0.2"
 
-postcss@^8.4.18:
-  version "8.4.19"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.19.tgz#61178e2add236b17351897c8bcc0b4c8ecab56fc"
-  integrity sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==
+postcss@^8.4.20:
+  version "8.4.20"
+  resolved "https://registry.npmmirror.com/postcss/-/postcss-8.4.20.tgz#64c52f509644cecad8567e949f4081d98349dc56"
+  integrity sha512-6Q04AXR1212bXr5fh03u8aAwbLxAQNGQ/Q1LNa0VfOI06ZAlhPHtQvE4OIdpj4kLThXilalPnmDSOD65DcHt+g==
   dependencies:
     nanoid "^3.3.4"
     picocolors "^1.0.0"
@@ -2642,10 +2642,10 @@ rollup@^2.75.6:
   optionalDependencies:
     fsevents "~2.3.2"
 
-rollup@^2.79.1:
-  version "2.79.1"
-  resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7"
-  integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==
+rollup@^3.7.0:
+  version "3.9.1"
+  resolved "https://registry.npmmirror.com/rollup/-/rollup-3.9.1.tgz#27501d3d026418765fe379d5620d25954ff2a011"
+  integrity sha512-GswCYHXftN8ZKGVgQhTFUJB/NBXxrRGgO2NCy6E8s1rwEJ4Q9/VttNqcYfEvx4dTo4j58YqdC3OVztPzlKSX8w==
   optionalDependencies:
     fsevents "~2.3.2"
 
@@ -2972,15 +2972,15 @@ vite@^3.0.4:
   optionalDependencies:
     fsevents "~2.3.2"
 
-vite@^3.2.3:
-  version "3.2.3"
-  resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.3.tgz#7a68d9ef73eff7ee6dc0718ad3507adfc86944a7"
-  integrity sha512-h8jl1TZ76eGs3o2dIBSsvXDLb1m/Ec1iej8ZMdz+PsaFUsftZeWe2CZOI3qogEsMNaywc17gu0q6cQDzh/weCQ==
+vite@^4.0.3:
+  version "4.0.3"
+  resolved "https://registry.npmmirror.com/vite/-/vite-4.0.3.tgz#de27ad3f263a03ae9419cdc8bc07721eadcba8b9"
+  integrity sha512-HvuNv1RdE7deIfQb8mPk51UKjqptO/4RXZ5yXSAvurd5xOckwS/gg8h9Tky3uSbnjYTgUm0hVCet1cyhKd73ZA==
   dependencies:
-    esbuild "^0.15.9"
-    postcss "^8.4.18"
+    esbuild "^0.16.3"
+    postcss "^8.4.20"
     resolve "^1.22.1"
-    rollup "^2.79.1"
+    rollup "^3.7.0"
   optionalDependencies:
     fsevents "~2.3.2"
 

+ 2 - 2
go.mod

@@ -5,7 +5,6 @@ go 1.19
 require (
 	github.com/creack/pty v1.1.18
 	github.com/dustin/go-humanize v1.0.0
-	github.com/emirpasic/gods v1.18.1
 	github.com/gin-contrib/static v0.0.1
 	github.com/gin-gonic/gin v1.7.4
 	github.com/go-acme/lego/v4 v4.4.0
@@ -16,9 +15,11 @@ require (
 	github.com/golang-jwt/jwt v3.2.2+incompatible
 	github.com/google/uuid v1.1.1
 	github.com/gorilla/websocket v1.4.2
+	github.com/hpcloud/tail v1.0.0
 	github.com/pkg/errors v0.9.1
 	github.com/shirou/gopsutil/v3 v3.21.7
 	github.com/spf13/cast v1.3.1
+	github.com/tufanbarisyildirim/gonginx v0.0.0-20230104065106-9ae864d29eed
 	github.com/unknwon/com v1.0.1
 	golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
 	gopkg.in/ini.v1 v1.62.0
@@ -32,7 +33,6 @@ require (
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-ole/go-ole v1.2.5 // indirect
 	github.com/golang/protobuf v1.3.4 // indirect
-	github.com/hpcloud/tail v1.0.0 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.2 // indirect
 	github.com/json-iterator/go v1.1.9 // indirect

+ 17 - 3
go.sum

@@ -23,6 +23,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
 cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
 contrib.go.opencensus.io/exporter/ocagent v0.4.12/go.mod h1:450APlNTSR6FrvC3CTRqYosuDstRB9un7SOx2k/9ckA=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+github.com/0xJacky/gonginx v0.0.0-20230104051937-4c3a63627efb h1:UzbGgIvP2UXpqlPG0ylT8/y0TIl5tBvAIeI3OAChFHI=
+github.com/0xJacky/gonginx v0.0.0-20230104051937-4c3a63627efb/go.mod h1:+uQMU+LMBHOQermcm/ICplG+r35Ypb6Up9iYKlvKuTE=
 github.com/Azure/azure-sdk-for-go v32.4.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
 github.com/Azure/go-autorest/autorest v0.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
 github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
@@ -97,14 +99,13 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
 github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
-github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
-github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/exoscale/egoscale v0.46.0/go.mod h1:mpEXBpROAa/2i5GC0r33rfxG+TxSEka11g1PIXt9+zc=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -172,8 +173,10 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
 github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
 github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
 github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-github/v32 v32.1.0/go.mod h1:rIEpZD9CTDQwDK9GDrtMTycQNA4JU3qBsCizh3q2WCI=
 github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -429,6 +432,8 @@ github.com/tklauser/numcpus v0.2.3 h1:nQ0QYpiritP6ViFhrKYsiv6VVxOpum2Gks5GhnJbS/
 github.com/tklauser/numcpus v0.2.3/go.mod h1:vpEPS/JC+oZGGQ/My/vJnNsvMDQL6PwOqt8dsCw5j+E=
 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
 github.com/transip/gotransip/v6 v6.2.0/go.mod h1:pQZ36hWWRahCUXkFWlx9Hs711gLd8J4qdgLdRzmtY+g=
+github.com/tufanbarisyildirim/gonginx v0.0.0-20230104065106-9ae864d29eed h1:EyT9V+boG4nI4pzIuN4AWHQNvyM1LxNS21MC1CDSfg4=
+github.com/tufanbarisyildirim/gonginx v0.0.0-20230104065106-9ae864d29eed/go.mod h1:+uQMU+LMBHOQermcm/ICplG+r35Ypb6Up9iYKlvKuTE=
 github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
@@ -446,6 +451,7 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2
 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
 github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
@@ -502,6 +508,7 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
 golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -529,6 +536,7 @@ golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU=
 golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -543,6 +551,7 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180622082034-63fc586f45fe/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -589,6 +598,7 @@ golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201110211018-35f3e6cf4a65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
@@ -641,9 +651,11 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK
 golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
 golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
@@ -725,6 +737,8 @@ gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9D
 gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
 gorm.io/gorm v1.21.14 h1:NAR9A/3SoyiPVHouW/rlpMUZvuQZ6Z6UYGz+2tosSQo=
 gorm.io/gorm v1.21.14/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
+gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I=
+gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

+ 310 - 98
server/api/cert.go

@@ -1,140 +1,352 @@
 package api
 
 import (
-	"github.com/0xJacky/Nginx-UI/server/model"
-	"github.com/0xJacky/Nginx-UI/server/pkg/cert"
-	"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
-	"github.com/gin-gonic/gin"
-	"github.com/gorilla/websocket"
-	"log"
-	"net/http"
+    "github.com/0xJacky/Nginx-UI/server/model"
+    "github.com/0xJacky/Nginx-UI/server/pkg/cert"
+    "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+    "github.com/gin-gonic/gin"
+    "github.com/gorilla/websocket"
+    "github.com/spf13/cast"
+    "log"
+    "net/http"
+    "os"
+    "path/filepath"
+    "strings"
 )
 
 const (
-	Success = "success"
-	Info    = "info"
-	Error   = "error"
+    Success = "success"
+    Info    = "info"
+    Error   = "error"
 )
 
 type IssueCertResponse struct {
-	Status            string `json:"status"`
-	Message           string `json:"message"`
-	SSLCertificate    string `json:"ssl_certificate,omitempty"`
-	SSLCertificateKey string `json:"ssl_certificate_key,omitempty"`
+    Status            string `json:"status"`
+    Message           string `json:"message"`
+    SSLCertificate    string `json:"ssl_certificate,omitempty"`
+    SSLCertificateKey string `json:"ssl_certificate_key,omitempty"`
 }
 
 func handleIssueCertLogChan(conn *websocket.Conn, logChan chan string) {
-	defer func() {
-		if err := recover(); err != nil {
-			log.Println("api.handleIssueCertLogChan recover", err)
-		}
-	}()
+    defer func() {
+        if err := recover(); err != nil {
+            log.Println("api.handleIssueCertLogChan recover", err)
+        }
+    }()
 
-	for logString := range logChan {
+    for logString := range logChan {
 
-		err := conn.WriteJSON(IssueCertResponse{
-			Status:  Info,
-			Message: logString,
-		})
+        err := conn.WriteJSON(IssueCertResponse{
+            Status:  Info,
+            Message: logString,
+        })
 
-		if err != nil {
-			log.Println("Error handleIssueCertLogChan", err)
-			return
-		}
+        if err != nil {
+            log.Println("Error handleIssueCertLogChan", err)
+            return
+        }
 
-	}
+    }
 }
 
 func IssueCert(c *gin.Context) {
-	domain := c.Param("domain")
+    var upGrader = websocket.Upgrader{
+        CheckOrigin: func(r *http.Request) bool {
+            return true
+        },
+    }
 
-	var upGrader = websocket.Upgrader{
-		CheckOrigin: func(r *http.Request) bool {
-			return true
-		},
-	}
+    // upgrade http to websocket
+    ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
+    if err != nil {
+        log.Println(err)
+        return
+    }
 
-	// upgrade http to websocket
-	ws, err := upGrader.Upgrade(c.Writer, c.Request, nil)
-	if err != nil {
-		log.Println(err)
-		return
-	}
+    defer func(ws *websocket.Conn) {
+        err := ws.Close()
+        if err != nil {
+            log.Println("defer websocket close err", err)
+        }
+    }(ws)
 
-	defer func(ws *websocket.Conn) {
-		err := ws.Close()
-		if err != nil {
-			log.Println("defer websocket close err", err)
-		}
-	}(ws)
+    // read
+    var buffer struct {
+        ServerName []string `json:"server_name"`
+    }
 
-	// read
-	mt, message, err := ws.ReadMessage()
+    err = ws.ReadJSON(&buffer)
 
-	if err != nil {
-		log.Println(err)
-		return
-	}
+    if err != nil {
+        log.Println(err)
+        return
+    }
 
-	if mt != websocket.TextMessage || string(message) != "go" {
-		return
-	}
+    logChan := make(chan string, 1)
+    errChan := make(chan error, 1)
 
-	logChan := make(chan string, 1)
-	errChan := make(chan error, 1)
+    go cert.IssueCert(buffer.ServerName, logChan, errChan)
 
-	go cert.IssueCert(domain, logChan, errChan)
+    domain := strings.Join(buffer.ServerName, "_")
 
-	go handleIssueCertLogChan(ws, logChan)
+    go handleIssueCertLogChan(ws, logChan)
 
-	// block, unless errChan closed
-	for err = range errChan {
-		log.Println("Error cert.IssueCert", err)
+    // block, unless errChan closed
+    for err = range errChan {
+        log.Println("Error cert.IssueCert", err)
 
-		err = ws.WriteJSON(IssueCertResponse{
-			Status:  Error,
-			Message: err.Error(),
-		})
+        err = ws.WriteJSON(IssueCertResponse{
+            Status:  Error,
+            Message: err.Error(),
+        })
 
-		if err != nil {
-			log.Println(err)
-			return
-		}
+        if err != nil {
+            log.Println("Error WriteJSON", err)
+            return
+        }
 
-		return
-	}
+        return
+    }
 
-	close(logChan)
+    close(logChan)
 
-	sslCertificatePath := nginx.GetNginxConfPath("ssl/" + domain + "/fullchain.cer")
-	sslCertificateKeyPath := nginx.GetNginxConfPath("ssl/" + domain + "/" + domain + ".key")
+    sslCertificatePath := nginx.GetNginxConfPath("ssl/" + domain + "/fullchain.cer")
+    sslCertificateKeyPath := nginx.GetNginxConfPath("ssl/" + domain + "/private.key")
 
-	certModel, err := model.FirstCert(domain)
+    certModel, err := model.FirstOrCreateCert(domain)
 
-	if err != nil {
-		log.Println(err)
-		return
-	}
+    if err != nil {
+        log.Println(err)
+    }
 
-	err = certModel.Updates(&model.Cert{
-		SSLCertificatePath: sslCertificatePath,
-	})
+    err = certModel.Updates(&model.Cert{
+        SSLCertificatePath:    sslCertificatePath,
+        SSLCertificateKeyPath: sslCertificateKeyPath,
+    })
 
-	if err != nil {
-		log.Println(err)
-		return
-	}
+    if err != nil {
+        log.Println(err)
+    }
 
-	err = ws.WriteJSON(IssueCertResponse{
-		Status:            Success,
-		Message:           "Issued certificate successfully",
-		SSLCertificate:    sslCertificatePath,
-		SSLCertificateKey: sslCertificateKeyPath,
-	})
+    err = ws.WriteJSON(IssueCertResponse{
+        Status:            Success,
+        Message:           "Issued certificate successfully",
+        SSLCertificate:    sslCertificatePath,
+        SSLCertificateKey: sslCertificateKeyPath,
+    })
 
-	if err != nil {
-		log.Println(err)
-		return
-	}
+    if err != nil {
+        log.Println(err)
+        return
+    }
 
 }
+
+func GetCertList(c *gin.Context) {
+    certList := model.GetCertList(c.Query("name"), c.Query("domain"))
+
+    c.JSON(http.StatusOK, gin.H{
+        "data": certList,
+    })
+}
+
+func getCert(c *gin.Context, certModel model.Cert) {
+    type resp struct {
+        model.Cert
+        SSLCertification    string           `json:"ssl_certification"`
+        SSLCertificationKey string           `json:"ssl_certification_key"`
+        CertificateInfo     *CertificateInfo `json:"certificate_info,omitempty"`
+    }
+
+    var sslCertificationBytes, sslCertificationKeyBytes []byte
+    var certificateInfo *CertificateInfo
+    if certModel.SSLCertificatePath != "" {
+        if _, err := os.Stat(certModel.SSLCertificatePath); err == nil {
+            sslCertificationBytes, _ = os.ReadFile(certModel.SSLCertificatePath)
+        }
+
+        pubKey, err := cert.GetCertInfo(certModel.SSLCertificatePath)
+
+        if err != nil {
+            ErrHandler(c, err)
+            return
+        }
+
+        certificateInfo = &CertificateInfo{
+            SubjectName: pubKey.Subject.CommonName,
+            IssuerName:  pubKey.Issuer.CommonName,
+            NotAfter:    pubKey.NotAfter,
+            NotBefore:   pubKey.NotBefore,
+        }
+    }
+
+    if certModel.SSLCertificateKeyPath != "" {
+        if _, err := os.Stat(certModel.SSLCertificateKeyPath); err == nil {
+            sslCertificationKeyBytes, _ = os.ReadFile(certModel.SSLCertificateKeyPath)
+        }
+    }
+
+    c.JSON(http.StatusOK, resp{
+        certModel,
+        string(sslCertificationBytes),
+        string(sslCertificationKeyBytes),
+        certificateInfo,
+    })
+}
+
+func GetCert(c *gin.Context) {
+    certModel, err := model.FirstCertByID(cast.ToInt(c.Param("id")))
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    getCert(c, certModel)
+}
+
+func AddCert(c *gin.Context) {
+    var json struct {
+        Name                  string `json:"name"`
+        Domain                string `json:"domain" binding:"required"`
+        SSLCertificatePath    string `json:"ssl_certificate_path" binding:"required"`
+        SSLCertificateKeyPath string `json:"ssl_certificate_key_path" binding:"required"`
+        SSLCertification      string `json:"ssl_certification"`
+        SSLCertificationKey   string `json:"ssl_certification_key"`
+    }
+    if !BindAndValid(c, &json) {
+        return
+    }
+    certModel, err := model.FirstOrCreateCert(json.Domain)
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    err = certModel.Updates(&model.Cert{
+        Name:                  json.Name,
+        Domain:                json.Domain,
+        SSLCertificatePath:    json.SSLCertificatePath,
+        SSLCertificateKeyPath: json.SSLCertificateKeyPath,
+    })
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    err = os.MkdirAll(filepath.Dir(json.SSLCertificatePath), 0644)
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    err = os.MkdirAll(filepath.Dir(json.SSLCertificateKeyPath), 0644)
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    if json.SSLCertification != "" {
+        err = os.WriteFile(json.SSLCertificatePath, []byte(json.SSLCertification), 0644)
+        if err != nil {
+            ErrHandler(c, err)
+            return
+        }
+    }
+
+    if json.SSLCertificationKey != "" {
+        err = os.WriteFile(json.SSLCertificateKeyPath, []byte(json.SSLCertificationKey), 0644)
+        if err != nil {
+            ErrHandler(c, err)
+            return
+        }
+    }
+
+    getCert(c, certModel)
+}
+
+func ModifyCert(c *gin.Context) {
+    id := cast.ToInt(c.Param("id"))
+    certModel, err := model.FirstCertByID(id)
+
+    var json struct {
+        Name                  string `json:"name"`
+        Domain                string `json:"domain" binding:"required"`
+        SSLCertificatePath    string `json:"ssl_certificate_path" binding:"required"`
+        SSLCertificateKeyPath string `json:"ssl_certificate_key_path" binding:"required"`
+        SSLCertification      string `json:"ssl_certification"`
+        SSLCertificationKey   string `json:"ssl_certification_key"`
+    }
+
+    if !BindAndValid(c, &json) {
+        return
+    }
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    err = certModel.Updates(&model.Cert{
+        Name:                  json.Name,
+        Domain:                json.Domain,
+        SSLCertificatePath:    json.SSLCertificatePath,
+        SSLCertificateKeyPath: json.SSLCertificateKeyPath,
+    })
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    err = os.MkdirAll(filepath.Dir(json.SSLCertificatePath), 0644)
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    err = os.MkdirAll(filepath.Dir(json.SSLCertificateKeyPath), 0644)
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    if json.SSLCertification != "" {
+        err = os.WriteFile(json.SSLCertificatePath, []byte(json.SSLCertification), 0644)
+        if err != nil {
+            ErrHandler(c, err)
+            return
+        }
+    }
+
+    if json.SSLCertificationKey != "" {
+        err = os.WriteFile(json.SSLCertificateKeyPath, []byte(json.SSLCertificationKey), 0644)
+        if err != nil {
+            ErrHandler(c, err)
+            return
+        }
+    }
+
+    GetCert(c)
+}
+
+func RemoveCert(c *gin.Context) {
+    id := cast.ToInt(c.Param("id"))
+    certModel, err := model.FirstCertByID(id)
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    err = certModel.Remove()
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    c.JSON(http.StatusNoContent, nil)
+}

+ 4 - 4
server/api/config.go

@@ -14,13 +14,15 @@ import (
 func GetConfigs(c *gin.Context) {
 	orderBy := c.Query("order_by")
 	sort := c.DefaultQuery("sort", "desc")
+	dir := c.DefaultQuery("dir", "/")
 
 	mySort := map[string]string{
 		"name":   "string",
 		"modify": "time",
+		"is_dir": "bool",
 	}
 
-	configFiles, err := os.ReadDir(nginx.GetNginxConfPath("/"))
+	configFiles, err := os.ReadDir(nginx.GetNginxConfPath(dir))
 
 	if err != nil {
 		ErrHandler(c, err)
@@ -56,14 +58,13 @@ func GetConfigs(c *gin.Context) {
 			if targetInfo.IsDir() {
 				continue
 			}
-		default:
-			continue
 		}
 
 		configs = append(configs, gin.H{
 			"name":   file.Name(),
 			"size":   fileInfo.Size(),
 			"modify": fileInfo.ModTime(),
+			"is_dir": file.IsDir(),
 		})
 	}
 
@@ -109,7 +110,6 @@ func AddConfig(c *gin.Context) {
 
 	path := filepath.Join(nginx.GetNginxConfPath("/"), name)
 
-	log.Println(path)
 	if _, err = os.Stat(path); err == nil {
 		c.JSON(http.StatusNotAcceptable, gin.H{
 			"message": "config exist",

+ 82 - 11
server/api/domain.go

@@ -77,7 +77,15 @@ type CertificateInfo struct {
 }
 
 func GetDomain(c *gin.Context) {
+	rewriteName, ok := c.Get("rewriteConfigFileName")
+
 	name := c.Param("name")
+
+	// for modify filename
+	if ok {
+		name = rewriteName.(string)
+	}
+
 	path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
 
 	enabled := true
@@ -93,8 +101,15 @@ func GetDomain(c *gin.Context) {
 	}
 
 	certInfoMap := make(map[int]CertificateInfo)
+	var serverName string
 	for serverIdx, server := range config.Servers {
 		for _, directive := range server.Directives {
+
+			if directive.Directive == "server_name" {
+				serverName = strings.ReplaceAll(directive.Params, " ", "_")
+				continue
+			}
+
 			if directive.Directive == "ssl_certificate" {
 
 				pubKey, err := cert.GetCertInfo(directive.Params)
@@ -116,33 +131,73 @@ func GetDomain(c *gin.Context) {
 		}
 	}
 
-	_, err = model.FirstCert(name)
+	certModel, _ := model.FirstCert(serverName)
 
 	c.JSON(http.StatusOK, gin.H{
 		"enabled":   enabled,
 		"name":      name,
-		"config":    config.BuildConfig(),
+		"config":    config.FmtCode(),
 		"tokenized": config,
-		"auto_cert": err == nil,
+		"auto_cert": certModel.AutoCert == model.AutoCertEnabled,
 		"cert_info": certInfoMap,
 	})
 
 }
 
 func EditDomain(c *gin.Context) {
-	var err error
 	name := c.Param("name")
-	request := make(gin.H)
-	err = c.BindJSON(&request)
+
+	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"`
+	}
+
+	if !BindAndValid(c, &json) {
+		return
+	}
+
 	path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
 
-	err = os.WriteFile(path, []byte(request["content"].(string)), 0644)
+	err := os.WriteFile(path, []byte(json.Content), 0644)
 	if err != nil {
 		ErrHandler(c, err)
 		return
 	}
-
 	enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
+	// rename the config file if needed
+	if name != json.Name {
+		newPath := filepath.Join(nginx.GetNginxConfPath("sites-available"), json.Name)
+		// recreate soft link
+		log.Println(enabledConfigFilePath)
+		if _, err = os.Stat(enabledConfigFilePath); err == nil {
+			log.Println(enabledConfigFilePath)
+			_ = os.Remove(enabledConfigFilePath)
+			enabledConfigFilePath = filepath.Join(nginx.GetNginxConfPath("sites-enabled"), json.Name)
+			err = os.Symlink(newPath, enabledConfigFilePath)
+
+			if err != nil {
+				ErrHandler(c, err)
+				return
+			}
+		}
+		err = os.Rename(path, newPath)
+		if err != nil {
+			ErrHandler(c, err)
+			return
+		}
+		name = json.Name
+		c.Set("rewriteConfigFileName", name)
+
+	}
+
+	enabledConfigFilePath = filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
 	if _, err = os.Stat(enabledConfigFilePath); err == nil {
 		// Test nginx configuration
 		err = nginx.TestNginxConf()
@@ -287,20 +342,36 @@ func DeleteDomain(c *gin.Context) {
 
 func AddDomainToAutoCert(c *gin.Context) {
 	domain := c.Param("domain")
-
+	domain = strings.ReplaceAll(domain, " ", "_")
 	certModel, err := model.FirstOrCreateCert(domain)
+
 	if err != nil {
 		ErrHandler(c, err)
 		return
 	}
+
+	err = certModel.Updates(&model.Cert{
+		AutoCert: model.AutoCertEnabled,
+	})
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
 	c.JSON(http.StatusOK, certModel)
 }
 
 func RemoveDomainFromAutoCert(c *gin.Context) {
+	domain := c.Param("domain")
+	domain = strings.ReplaceAll(domain, " ", "_")
 	certModel := model.Cert{
-		Domain: c.Param("domain"),
+		Domain: domain,
 	}
-	err := certModel.Remove()
+
+	err := certModel.Updates(&model.Cert{
+		AutoCert: model.AutoCertDisabled,
+	})
 
 	if err != nil {
 		ErrHandler(c, err)

+ 0 - 1
server/api/nginx_log.go

@@ -125,7 +125,6 @@ func getLogPath(control *controlStruct) (logPath string, err error) {
 		}
 
 		directive := config.Servers[control.ServerIdx].Directives[control.DirectiveIdx]
-
 		switch directive.Directive {
 		case "access_log", "error_log":
 			// ok

+ 31 - 26
server/api/ngx.go

@@ -1,42 +1,47 @@
 package api
 
 import (
-	"bufio"
-	nginx2 "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
-	"github.com/gin-gonic/gin"
-	"net/http"
-	"strings"
+    "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+    "github.com/gin-gonic/gin"
+    "net/http"
 )
 
 func BuildNginxConfig(c *gin.Context) {
-	var ngxConf nginx2.NgxConfig
-	if !BindAndValid(c, &ngxConf) {
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"content": ngxConf.BuildConfig(),
-	})
+    var ngxConf nginx.NgxConfig
+    if !BindAndValid(c, &ngxConf) {
+        return
+    }
+
+    c.JSON(http.StatusOK, gin.H{
+        "content": ngxConf.BuildConfig(),
+    })
 }
 
 func TokenizeNginxConfig(c *gin.Context) {
-	var json struct {
-		Content string `json:"content" binding:"required"`
-	}
+    var json struct {
+        Content string `json:"content" binding:"required"`
+    }
 
-	if !BindAndValid(c, &json) {
-		return
-	}
+    if !BindAndValid(c, &json) {
+        return
+    }
 
-	scanner := bufio.NewScanner(strings.NewReader(json.Content))
+    ngxConfig := nginx.ParseNgxConfigByContent(json.Content)
 
-	ngxConfig, err := nginx2.ParseNgxConfigByScanner("", scanner)
+    c.JSON(http.StatusOK, ngxConfig)
 
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
+}
+
+func FormatNginxConfig(c *gin.Context) {
+    var json struct {
+        Content string `json:"content" binding:"required"`
+    }
 
-	c.JSON(http.StatusOK, ngxConfig)
+    if !BindAndValid(c, &json) {
+        return
+    }
 
+    c.JSON(http.StatusOK, gin.H{
+        "content": nginx.FmtCode(json.Content),
+    })
 }

+ 38 - 0
server/api/settings.go

@@ -0,0 +1,38 @@
+package api
+
+import (
+	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/gin-gonic/gin"
+	"net/http"
+)
+
+func GetSettings(c *gin.Context) {
+	c.JSON(http.StatusOK, gin.H{
+		"server":    settings.ServerSettings,
+		"nginx_log": settings.NginxLogSettings,
+	})
+}
+
+func SaveSettings(c *gin.Context) {
+	var json struct {
+		Server   settings.Server   `json:"server"`
+		NginxLog settings.NginxLog `json:"nginx_log"`
+	}
+
+	if !BindAndValid(c, &json) {
+		return
+	}
+
+	settings.Conf.Section("server").Key("Email").SetValue(json.Server.Email)
+	settings.Conf.Section("server").Key("HTTPChallengePort").SetValue(json.Server.HTTPChallengePort)
+	settings.Conf.Section("nginx_log").Key("AccessLogPath").SetValue(json.NginxLog.AccessLogPath)
+	settings.Conf.Section("nginx_log").Key("ErrorLogPath").SetValue(json.NginxLog.ErrorLogPath)
+
+	err := settings.Save()
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	GetSettings(c)
+}

+ 43 - 0
server/api/template.go

@@ -2,6 +2,7 @@ package api
 
 import (
 	"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+	"github.com/0xJacky/Nginx-UI/server/service"
 	"github.com/0xJacky/Nginx-UI/server/settings"
 	"github.com/gin-gonic/gin"
 	"net/http"
@@ -57,3 +58,45 @@ proxy_pass http://127.0.0.1:{{ HTTP01PORT }};
 		"tokenized": ngxConfig,
 	})
 }
+
+func GetTemplateConfList(c *gin.Context) {
+	configList, err := service.GetTemplateList("conf")
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
+	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,
+	})
+}

+ 29 - 3
server/model/cert.go

@@ -6,10 +6,18 @@ import (
 	"path/filepath"
 )
 
+const (
+	AutoCertEnabled  = 1
+	AutoCertDisabled = -1
+)
+
 type Cert struct {
 	Model
-	Domain             string `json:"domain"`
-	SSLCertificatePath string `json:"ssl_certificate_path"`
+	Name                  string `json:"name"`
+	Domain                string `json:"domain"`
+	SSLCertificatePath    string `json:"ssl_certificate_path"`
+	SSLCertificateKeyPath string `json:"ssl_certificate_key_path"`
+	AutoCert              int    `json:"auto_cert"`
 }
 
 func FirstCert(domain string) (c Cert, err error) {
@@ -27,7 +35,7 @@ func FirstOrCreateCert(domain string) (c Cert, err error) {
 
 func GetAutoCertList() (c []Cert) {
 	var t []Cert
-	db.Find(&t)
+	db.Where("auto_cert", AutoCertEnabled).Find(&t)
 
 	// check if this domain is enabled
 	enabledConfig, err := os.ReadDir(filepath.Join(nginx.GetNginxConfPath("sites-enabled")))
@@ -50,6 +58,24 @@ func GetAutoCertList() (c []Cert) {
 	return
 }
 
+func GetCertList(name, domain string) (c []Cert) {
+	tx := db
+	if name != "" {
+		tx = tx.Where("name LIKE ? or domain LIKE ?", "%"+name+"%", "%"+name+"%")
+	}
+	if domain != "" {
+		tx = tx.Where("domain LIKE ?", "%"+domain+"%")
+	}
+	tx.Find(&c)
+	return
+}
+
+func FirstCertByID(id int) (c Cert, err error) {
+	err = db.First(&c, id).Error
+
+	return
+}
+
 func (c *Cert) Updates(n *Cert) error {
 	return db.Model(c).Updates(n).Error
 }

+ 3 - 1
server/pkg/cert/auto_cert.go

@@ -3,6 +3,7 @@ package cert
 import (
 	"github.com/0xJacky/Nginx-UI/server/model"
 	"log"
+	"strings"
 	"time"
 )
 
@@ -56,7 +57,8 @@ func AutoCert() {
 		logChan := make(chan string, 1)
 		errChan := make(chan error, 1)
 
-		go IssueCert(domain, logChan, errChan)
+		// support SAN certification
+		go IssueCert(strings.Split(domain, "_"), logChan, errChan)
 
 		go handleIssueCertLogChan(logChan)
 

+ 10 - 4
server/pkg/cert/cert.go

@@ -16,6 +16,7 @@ import (
 	"log"
 	"os"
 	"path/filepath"
+	"strings"
 )
 
 // MyUser You'll need a user or account type that implements acme.User
@@ -35,7 +36,7 @@ func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
 	return u.key
 }
 
-func IssueCert(domain string, logChan chan string, errChan chan error) {
+func IssueCert(domain []string, logChan chan string, errChan chan error) {
 	defer func() {
 		if err := recover(); err != nil {
 			log.Println("Issue Cert recover", err)
@@ -62,6 +63,10 @@ func IssueCert(domain string, logChan chan string, errChan chan error) {
 		config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
 	}
 
+	if settings.ServerSettings.CADir != "" {
+		config.CADirURL = settings.ServerSettings.CADir
+	}
+
 	config.Certificate.KeyType = certcrypto.RSA2048
 
 	logChan <- "Creating client facilitates communication with the CA server"
@@ -94,7 +99,7 @@ func IssueCert(domain string, logChan chan string, errChan chan error) {
 	myUser.Registration = reg
 
 	request := certificate.ObtainRequest{
-		Domains: []string{domain},
+		Domains: domain,
 		Bundle:  true,
 	}
 
@@ -104,7 +109,8 @@ func IssueCert(domain string, logChan chan string, errChan chan error) {
 		errChan <- errors.Wrap(err, "issue cert fail to obtain")
 		return
 	}
-	saveDir := nginx.GetNginxConfPath("ssl/" + domain)
+	name := strings.Join(domain, " ")
+	saveDir := nginx.GetNginxConfPath("ssl/" + name)
 	if _, err = os.Stat(saveDir); os.IsNotExist(err) {
 		err = os.MkdirAll(saveDir, 0755)
 		if err != nil {
@@ -125,7 +131,7 @@ func IssueCert(domain string, logChan chan string, errChan chan error) {
 	}
 
 	logChan <- "Writing certificate private key to disk"
-	err = os.WriteFile(filepath.Join(saveDir, domain+".key"),
+	err = os.WriteFile(filepath.Join(saveDir, "private.key"),
 		certificates.PrivateKey, 0644)
 
 	if err != nil {

+ 70 - 68
server/pkg/nginx/build_config.go

@@ -1,88 +1,90 @@
 package nginx
 
 import (
-    "bufio"
-    "fmt"
-    "strings"
+	"bufio"
+	"fmt"
+	"github.com/tufanbarisyildirim/gonginx"
+	"github.com/tufanbarisyildirim/gonginx/parser"
+	"strings"
 )
 
 func buildComments(orig string, indent int) (content string) {
-    scanner := bufio.NewScanner(strings.NewReader(orig))
-    for scanner.Scan() {
-        content += strings.Repeat("\t", indent) + "# " + scanner.Text() + "\n"
-    }
-    content = strings.TrimLeft(content, "\n")
-    return
+	scanner := bufio.NewScanner(strings.NewReader(orig))
+	for scanner.Scan() {
+		content += strings.Repeat("\t", indent) + "# " + scanner.Text() + "\n"
+	}
+	content = strings.TrimLeft(content, "\n")
+	return
 }
 
 func (c *NgxConfig) BuildConfig() (content string) {
 
-    // Custom
-    if c.Custom != "" {
-        content += fmtCode(c.Custom)
-        content += "\n\n"
-    }
+	// Custom
+	if c.Custom != "" {
+		content += c.Custom
+		content += "\n\n"
+	}
 
-    // Upstreams
-    for _, u := range c.Upstreams {
+	// Upstreams
+	for _, u := range c.Upstreams {
 
-        upstream := ""
-        var comments string
-        for _, directive := range u.Directives {
-            if directive.Comments != "" {
-                comments = buildComments(directive.Comments, 1)
-            }
-            upstream += fmt.Sprintf("%s\t%s;\n", comments, directive.Orig())
-        }
-        comments = buildComments(u.Comments, 1)
-        content += fmt.Sprintf("upstream %s {\n%s%s}\n\n", u.Name, comments, upstream)
-    }
+		upstream := ""
+		var comments string
+		for _, directive := range u.Directives {
+			if directive.Comments != "" {
+				comments = buildComments(directive.Comments, 1)
+			}
+			upstream += fmt.Sprintf("%s\t%s;\n", comments, directive.Orig())
+		}
+		comments = buildComments(u.Comments, 1)
+		content += fmt.Sprintf("upstream %s {\n%s%s}\n\n", u.Name, comments, upstream)
+	}
 
-    // Servers
-    for _, s := range c.Servers {
-        server := ""
+	// Servers
+	for _, s := range c.Servers {
+		server := ""
 
-        // directives
-        for _, directive := range s.Directives {
-            var comments string
-            if directive.Comments != "" {
-                comments = buildComments(directive.Comments, 1)
-            }
-            if directive.Directive == If {
-                server += fmt.Sprintf("%s%s\n", comments, fmtCodeWithIndent(directive.Params, 1))
-            } else if directive.Params != "" {
-                server += fmt.Sprintf("%s\t%s;\n", comments, directive.Orig())
-            }
-        }
+		// directives
+		for _, directive := range s.Directives {
+			var comments string
+			if directive.Comments != "" {
+				comments = buildComments(directive.Comments, 1)
+			}
+			if directive.Params != "" {
+				server += fmt.Sprintf("%s\t%s;\n", comments, directive.Orig())
+			}
+		}
 
-        if len(s.Directives) > 0 {
-            server += "\n"
-        }
+		if len(s.Directives) > 0 {
+			server += "\n"
+		}
 
-        // locations
-        locations := ""
-        for _, location := range s.Locations {
-            locationContent := ""
-            scanner := bufio.NewScanner(strings.NewReader(location.Content))
-            for scanner.Scan() {
-                locationContent += "\t\t" + scanner.Text() + "\n"
-            }
-            var comments string
-            if location.Comments != "" {
-                comments = buildComments(location.Comments, 1)
-            }
-            locations += fmt.Sprintf("%s\tlocation %s {\n%s\t}\n\n", comments, location.Path, locationContent)
-        }
+		// locations
+		locations := ""
+		for _, location := range s.Locations {
+			locationContent := ""
+			scanner := bufio.NewScanner(strings.NewReader(location.Content))
+			for scanner.Scan() {
+				locationContent += "\t\t" + scanner.Text() + "\n"
+			}
+			var comments string
+			if location.Comments != "" {
+				comments = buildComments(location.Comments, 1)
+			}
+			locations += fmt.Sprintf("%s\tlocation %s {\n%s\t}\n\n", comments, location.Path, locationContent)
+		}
 
-        server += locations
+		server += locations
 
-        var comments string
-        if s.Comments != "" {
-            comments = buildComments(s.Comments, 0) + "\n"
-        }
+		var comments string
+		if s.Comments != "" {
+			comments = buildComments(s.Comments, 0) + "\n"
+		}
 
-        content += fmt.Sprintf("%sserver {\n%s}\n\n", comments, server)
-    }
-
-    return
+		content += fmt.Sprintf("%sserver {\n%s}\n\n", comments, server)
+	}
+	p := parser.NewStringParser(content)
+	config := p.Parse()
+	content = gonginx.DumpConfig(config, gonginx.IndentedStyle)
+	return
 }

+ 0 - 0
server/test/nextcloud_ngx.conf → server/pkg/nginx/conf/nextcloud_ngx.conf


+ 36 - 0
server/pkg/nginx/conf/test.conf

@@ -0,0 +1,36 @@
+map $http_upgrade $connection_upgrade {
+	default upgrade;
+	'' close;
+}
+
+server {
+	listen 80;
+	listen [::]:80;
+	server_name blog.jackyu.cn test.jackyu.cn;
+
+	location /.well-known/acme-challenge {
+		proxy_set_header Host $host;
+		proxy_set_header X-Real_IP $remote_addr;
+		proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
+		proxy_pass http://127.0.0.1:9180;
+	}
+
+}
+
+server {
+	listen 443 ssl http2;
+	listen [::]:443 ssl http2;
+	server_name blog.jackyu.cn test.jackyu.cn;
+	ssl_certificate /etc/nginx/ssl/blog.jackyu.cn_test.jackyu.cn/fullchain.cer;
+	ssl_certificate_key /etc/nginx/ssl/blog.jackyu.cn_test.jackyu.cn/private.key;
+	include enable-php-8.conf;
+
+	location /.well-known/acme-challenge {
+		proxy_set_header Host $host;
+		proxy_set_header X-Real_IP $remote_addr;
+		proxy_set_header X-Forwarded-For $remote_addr:$remote_port;
+		proxy_pass http://127.0.0.1:9180;
+	}
+
+}
+

+ 10 - 41
server/pkg/nginx/format_code.go

@@ -1,49 +1,18 @@
 package nginx
 
 import (
-    "bufio"
-    "github.com/emirpasic/gods/stacks/linkedliststack"
-    "strings"
+	"github.com/tufanbarisyildirim/gonginx"
+	"github.com/tufanbarisyildirim/gonginx/parser"
 )
 
-func fmtCode(content string) (fmtContent string) {
-    fmtContent = fmtCodeWithIndent(content, 0)
-    return
+func (c *NgxConfig) FmtCode() (fmtContent string) {
+	fmtContent = gonginx.DumpConfig(c.c, gonginx.IndentedStyle)
+	return
 }
 
-func fmtCodeWithIndent(content string, indent int) (fmtContent string) {
-    /*
-       Format content
-       1. TrimSpace for each line
-       2. use stack to count how many \t should add
-    */
-    stack := linkedliststack.New()
-
-    scanner := bufio.NewScanner(strings.NewReader(content))
-
-    for scanner.Scan() {
-        text := scanner.Text()
-        text = strings.TrimSpace(text)
-
-        before := stack.Size()
-
-        for _, char := range text {
-            matchParentheses(stack, char)
-        }
-
-        after := stack.Size()
-
-        fmtContent += strings.Repeat("\t", indent)
-
-        if before == after {
-            fmtContent += strings.Repeat("\t", stack.Size()) + text + "\n"
-        } else {
-            fmtContent += text + "\n"
-        }
-
-    }
-
-    fmtContent = strings.Trim(fmtContent, "\n")
-
-    return
+func FmtCode(content string) (fmtContent string) {
+	p := parser.NewStringParser(content)
+	c := p.Parse()
+	fmtContent = gonginx.DumpConfig(c, gonginx.IndentedStyle)
+	return
 }

+ 49 - 0
server/pkg/nginx/ngx_conf_parse_test.go

@@ -0,0 +1,49 @@
+package nginx
+
+import (
+	"fmt"
+	"github.com/tufanbarisyildirim/gonginx"
+	"github.com/tufanbarisyildirim/gonginx/parser"
+	"strings"
+	"testing"
+)
+
+func TestNgxConfParse(t *testing.T) {
+	p, err := parser.NewParser("conf/nextcloud_ngx.conf")
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	n := p.Parse()
+
+	fn(n.Block, 0)
+
+	c, err := ParseNgxConfig("conf/nextcloud_ngx.conf")
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println(c)
+	c, err = ParseNgxConfig("conf/test.conf")
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	fmt.Println(c)
+}
+
+func fn(block gonginx.IBlock, deep int) {
+	if block == nil {
+		return
+	}
+	for _, v := range block.GetDirectives() {
+		if len(v.GetComment()) > 0 {
+			for _, c := range v.GetComment() {
+				fmt.Println(strings.Repeat("\t", deep), c)
+			}
+		}
+
+		fmt.Println(fmt.Sprintf("%s%s %s", strings.Repeat("\t", deep), v.GetName(), strings.Join(v.GetParameters(), " ")))
+		fn(v.GetBlock(), deep+1)
+	}
+}

+ 128 - 109
server/pkg/nginx/parse.go

@@ -1,150 +1,169 @@
 package nginx
 
 import (
-	"bufio"
-	"github.com/emirpasic/gods/stacks/linkedliststack"
 	"github.com/pkg/errors"
-	"os"
+	"github.com/tufanbarisyildirim/gonginx"
+	"github.com/tufanbarisyildirim/gonginx/parser"
 	"strings"
-	"unicode"
 )
 
 const (
-	Server       = "server"
-	Location     = "location"
-	Upstream     = "upstream"
-	CommentStart = "#"
-	Empty        = ""
-	If           = "if"
+	Server   = "server"
+	Location = "location"
+	Upstream = "upstream"
 )
 
-func matchParentheses(stack *linkedliststack.Stack, v int32) {
-	if v == '{' {
-		stack.Push(v)
-	} else if v == '}' {
-		// stack is not empty and the top is == '{'
-		if top, ok := stack.Peek(); ok && top == '{' {
-			stack.Pop()
-		} else {
-			// fail
-			stack.Push(v)
-		}
-	}
+func (s *NgxServer) ParseServer(directive gonginx.IDirective) {
+	s.parseServer(directive)
 }
 
-func parseDirective(scanner *bufio.Scanner) (d NgxDirective) {
-	text := strings.TrimSpace(scanner.Text())
-	// escape empty line or comment line
-	if len(text) < 1 {
+func (s *NgxServer) parseServer(directive gonginx.IDirective) {
+	if directive.GetBlock() == nil {
 		return
 	}
-
-	if text[0] == '#' {
-		d.Directive = "#"
-		d.Params = strings.TrimLeft(text, "#")
+	for _, d := range directive.GetBlock().GetDirectives() {
+		switch d.GetName() {
+		case Location:
+			location := &NgxLocation{
+				Path:     strings.Join(d.GetParameters(), " "),
+				Comments: buildComment(d.GetComment()),
+			}
+			location.parseLocation(d, 0)
+			s.Locations = append(s.Locations, location)
+		default:
+			dir := &NgxDirective{
+				Directive: d.GetName(),
+				Comments:  buildComment(d.GetComment()),
+			}
+			dir.parseDirective(d, 0)
+			s.Directives = append(s.Directives, dir)
+		}
+	}
+}
+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
 	}
-
-	if len(text) > 1 {
-		sep := len(text) - 1
-		for k, v := range text {
-			if unicode.IsSpace(v) {
-				sep = k
-				break
+	for _, location := range directive.GetBlock().GetDirectives() {
+		if len(location.GetComment()) > 0 {
+			for _, c := range location.GetComment() {
+				l.Content += strings.Repeat("\t", deep) + c + "\n"
 			}
 		}
-
-		d.Directive = text[0:sep]
-		d.Params = text[sep:]
-	} else {
-		d.Directive = text
-		return
+		l.Content += strings.Repeat("\t", deep) + location.GetName() + " " + strings.Join(location.GetParameters(), " ") + ";\n"
+		l.parseLocation(location, deep+1)
 	}
+}
 
-	stack := linkedliststack.New()
-
-	if d.Directive == Server || d.Directive == Upstream || d.Directive == Location || d.Directive == If {
-		// { } in one line
-		// location = /.well-known/carddav { return 301 /remote.php/dav/; }
-		if strings.Contains(d.Params, "{") {
-			for _, v := range d.Params {
-				matchParentheses(stack, v)
-			}
+func (d *NgxDirective) ParseDirective(directive gonginx.IDirective, deep int) {
+	d.parseDirective(directive, deep)
+}
 
-			if stack.Empty() {
-				return
+func (d *NgxDirective) parseDirective(directive gonginx.IDirective, deep int) {
+	if directive.GetBlock() != nil {
+		d.Params += directive.GetName() + " "
+		d.Directive = ""
+	}
+	d.Params += strings.Join(directive.GetParameters(), " ")
+	if directive.GetBlock() != nil {
+		d.Params += " {\n"
+		for _, location := range directive.GetBlock().GetDirectives() {
+			if len(location.GetComment()) > 0 {
+				for _, c := range location.GetComment() {
+					d.Params += strings.Repeat("\t", deep) + c + "\n"
+				}
 			}
-		}
-
-		// location ^~ /.well-known {
-		// location ^~ /.well-known
-		// {
-		// location ^~ /.well-known
-		//
-		//    {
-		// { } not in one line
-		for scanner.Scan() {
-			text = strings.TrimSpace(scanner.Text())
-			// escape empty line
-			if text == "" {
+			d.Params += strings.Repeat("\t", deep+1) + location.GetName() + " " +
+				strings.Join(location.GetParameters(), " ") + ";\n"
+			// d.parseDirective(location, deep+1)
+			if location.GetBlock() == nil {
 				continue
 			}
-			d.Params += "\n" + scanner.Text()
-			for _, v := range text {
-				matchParentheses(stack, v)
-				if stack.Empty() {
-					break
-				}
-			}
-			if stack.Empty() {
-				break
+			for _, v := range location.GetBlock().GetDirectives() {
+				d.parseDirective(v, deep+1)
 			}
 		}
+		d.Params += "}\n"
+		return
+	}
+}
+
+func (u *NgxUpstream) parseUpstream(directive gonginx.IDirective) {
+	if directive.GetBlock() == nil {
+		return
+	}
+	for _, us := range directive.GetBlock().GetDirectives() {
+		d := &NgxDirective{
+			Directive: us.GetName(),
+			Params:    strings.Join(us.GetParameters(), " "),
+			Comments:  buildComment(us.GetComment()),
+		}
+		u.Directives = append(u.Directives, d)
 	}
-	d.Params = strings.TrimSpace(d.Params)
-	return
 }
 
-func ParseNgxConfigByScanner(filename string, scanner *bufio.Scanner) (c *NgxConfig, err error) {
-	c = NewNgxConfig(filename)
+func (c *NgxConfig) parseCustom(directive gonginx.IDirective) {
+	if directive.GetBlock() == nil {
+		return
+	}
+	c.Custom += "{\n"
+	for _, v := range directive.GetBlock().GetDirectives() {
+		c.Custom += strings.Join(v.GetComment(), "\n") + "\n" +
+			v.GetName() + " " + strings.Join(v.GetParameters(), " ") + ";\n"
+	}
+	c.Custom += "}\n"
+}
+
+func buildComment(c []string) string {
+	return strings.ReplaceAll(strings.Join(c, "\n"), "#", "")
+}
 
-	for scanner.Scan() {
-		d := parseDirective(scanner)
-		paramsScanner := bufio.NewScanner(strings.NewReader(d.Params))
-		switch d.Directive {
+func parse(block gonginx.IBlock, ngxConfig *NgxConfig) {
+	if block == nil {
+		return
+	}
+	for _, v := range block.GetDirectives() {
+		comments := buildComment(v.GetComment())
+		switch v.GetName() {
 		case Server:
-			c.parseServer(paramsScanner)
+			server := NewNgxServer()
+			server.Comments = comments
+			server.parseServer(v)
+			ngxConfig.Servers = append(ngxConfig.Servers, server)
 		case Upstream:
-			c.parseUpstream(paramsScanner)
-		case CommentStart:
-			c.commentQueue.Enqueue(d.Params)
-		case Empty:
-			continue
+			upstream := &NgxUpstream{}
+			upstream.Comments = comments
+			upstream.parseUpstream(v)
+			ngxConfig.Upstreams = append(ngxConfig.Upstreams, upstream)
 		default:
-			c.Custom += d.Orig() + "\n"
+			ngxConfig.Custom += strings.Join(v.GetComment(), "\n") + "\n" +
+				v.GetName() + " " + strings.Join(v.GetParameters(), " ") + "\n"
+			ngxConfig.parseCustom(v)
 		}
 	}
+	ngxConfig.Custom = FmtCode(ngxConfig.Custom)
+}
 
-	if err = scanner.Err(); err != nil {
-		return nil, errors.Wrap(err, "error scanner in ParseNgxConfig")
-	}
-
-	// Attach the rest of the comments to the last server
-	if len(c.Servers) > 0 {
-		c.Servers[len(c.Servers)-1].Comments += c.commentQueue.DequeueAllComments()
-	}
-
-	return c, nil
+func ParseNgxConfigByContent(content string) (ngxConfig *NgxConfig) {
+	p := parser.NewStringParser(content)
+	c := p.Parse()
+	ngxConfig = NewNgxConfig("")
+	ngxConfig.c = c
+	parse(c.Block, ngxConfig)
+	return
 }
 
-func ParseNgxConfig(filename string) (c *NgxConfig, err error) {
-	file, err := os.Open(filename)
+func ParseNgxConfig(filename string) (ngxConfig *NgxConfig, err error) {
+	p, err := parser.NewParser(filename)
 	if err != nil {
-		return nil, errors.Wrap(err, "error open file in ParseNgxConfig")
+		return nil, errors.Wrap(err, "error ParseNgxConfig")
 	}
-	defer file.Close()
-
-	scanner := bufio.NewScanner(file)
-
-	return ParseNgxConfigByScanner(filename, scanner)
+	c := p.Parse()
+	ngxConfig = NewNgxConfig(filename)
+	ngxConfig.c = c
+	parse(c.Block, ngxConfig)
+	return
 }

+ 0 - 125
server/pkg/nginx/tokenize.go

@@ -1,125 +0,0 @@
-package nginx
-
-import (
-	"bufio"
-	"regexp"
-	"strings"
-	"unicode"
-)
-
-func (c *NgxConfig) parseServer(scanner *bufio.Scanner) {
-	server := NewNgxServer()
-	for scanner.Scan() {
-		d := parseDirective(scanner)
-		switch d.Directive {
-		case Location:
-			server.parseLocation(d.Params)
-		case CommentStart:
-			server.commentQueue.Enqueue(d.Params)
-		default:
-			server.parseDirective(d)
-		}
-	}
-	// Attach the rest of the comments to the last location
-	if len(server.Locations) > 0 {
-		server.Locations[len(server.Locations)-1].Comments += server.commentQueue.DequeueAllComments()
-	}
-
-	// Attach comments which are over the current server
-	server.Comments = c.commentQueue.DequeueAllComments()
-
-	c.Servers = append(c.Servers, server)
-}
-
-func (c *NgxConfig) parseUpstream(scanner *bufio.Scanner) {
-	upstream := &NgxUpstream{}
-	for scanner.Scan() {
-		d := NgxDirective{}
-		text := strings.TrimSpace(scanner.Text())
-		// escape empty line or comment line
-		if len(text) < 1 || text[0] == '#' {
-			return
-		}
-
-		sep := len(text) - 1
-		for k, v := range text {
-			if unicode.IsSpace(v) {
-				sep = k
-				break
-			}
-		}
-
-		d.Directive = text[0:sep]
-		d.Params = strings.Trim(text[sep:], ";")
-
-		if d.Directive == Server {
-			upstream.Directives = append(upstream.Directives, &d)
-		} else if upstream.Name == "" {
-			upstream.Name = d.Directive
-		}
-	}
-	// attach comments which are over the current upstream
-	upstream.Comments = c.commentQueue.DequeueAllComments()
-
-	c.Upstreams = append(c.Upstreams, upstream)
-}
-
-func (s *NgxServer) parseDirective(d NgxDirective) {
-	orig := d.Orig()
-	// handle inline comments
-	str, comments, _ := strings.Cut(orig, "#")
-
-	if d.Directive == If {
-		d.Params = "if " + d.Params
-		d.Params = fmtCode(d.Params)
-		s.Directives = append(s.Directives, &d)
-		return
-	}
-
-	regExp := regexp.MustCompile("(\\S+?)\\s+?{?(.+?)[;|}]")
-	matchSlice := regExp.FindAllStringSubmatch(str, -1)
-
-	for k, v := range matchSlice {
-		// [[gzip_min_length 256; gzip_min_length 256] [gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; gzip_proxied expired no-cache no-store private no_last_modified no_etag auth] [gzip on; gzip on] [gzip_vary on; gzip_vary on] [location /x/ {} location /x/ {] [gzip_comp_level 4; gzip_comp_level 4]]
-		if len(v) > 0 {
-			scanner := bufio.NewScanner(strings.NewReader(v[0]))
-			if scanner.Scan() {
-				d = parseDirective(scanner)
-				// inline location
-				if d.Directive == Location {
-					s.parseLocation(d.Orig())
-				} else {
-
-					if k == 0 {
-						d.Comments = s.commentQueue.DequeueAllComments()
-					} else if k == len(matchSlice)-1 {
-						d.Comments = comments
-					}
-
-					// trim right ';'
-					d.TrimParams()
-					// map[directive]=>[]Params
-					s.Directives = append(s.Directives, &d)
-				}
-
-			}
-		}
-	}
-}
-
-func (s *NgxServer) parseLocation(str string) {
-	path, content, _ := strings.Cut(str, "{")
-	path = strings.TrimSpace(path)
-
-	content = strings.TrimSpace(content)
-	content = strings.Trim(content, "}")
-
-	content = fmtCode(content)
-
-	location := &NgxLocation{
-		Path:    path,
-		Content: content,
-	}
-	location.Comments = s.commentQueue.DequeueAllComments()
-	s.Locations = append(s.Locations, location)
-}

+ 13 - 32
server/pkg/nginx/type.go

@@ -1,27 +1,22 @@
 package nginx
 
 import (
-	"github.com/emirpasic/gods/queues/linkedlistqueue"
+	"github.com/tufanbarisyildirim/gonginx"
 	"strings"
 )
 
-type CommentQueue struct {
-	*linkedlistqueue.Queue
-}
-
 type NgxConfig struct {
-	FileName     string         `json:"file_name"`
-	Upstreams    []*NgxUpstream `json:"upstreams"`
-	Servers      []*NgxServer   `json:"servers"`
-	Custom       string         `json:"custom"`
-	commentQueue *CommentQueue
+	FileName  string         `json:"file_name"`
+	Upstreams []*NgxUpstream `json:"upstreams"`
+	Servers   []*NgxServer   `json:"servers"`
+	Custom    string         `json:"custom"`
+	c         *gonginx.Config
 }
 
 type NgxServer struct {
-	Directives   []*NgxDirective `json:"directives"`
-	Locations    []*NgxLocation  `json:"locations"`
-	Comments     string          `json:"comments"`
-	commentQueue *CommentQueue
+	Directives []*NgxDirective `json:"directives"`
+	Locations  []*NgxLocation  `json:"locations"`
+	Comments   string          `json:"comments"`
 }
 
 type NgxUpstream struct {
@@ -42,18 +37,6 @@ type NgxLocation struct {
 	Comments string `json:"comments"`
 }
 
-func (c *CommentQueue) DequeueAllComments() (comments string) {
-	for !c.Empty() {
-		comment, ok := c.Dequeue()
-
-		if ok {
-			comments += strings.TrimSpace(comment.(string)) + "\n"
-		}
-	}
-
-	return
-}
-
 func (d *NgxDirective) Orig() string {
 	return d.Directive + " " + d.Params
 }
@@ -65,16 +48,14 @@ func (d *NgxDirective) TrimParams() {
 
 func NewNgxServer() *NgxServer {
 	return &NgxServer{
-		Locations:    make([]*NgxLocation, 0),
-		Directives:   make([]*NgxDirective, 0),
-		commentQueue: &CommentQueue{linkedlistqueue.New()},
+		Locations:  make([]*NgxLocation, 0),
+		Directives: make([]*NgxDirective, 0),
 	}
 }
 
 func NewNgxConfig(filename string) *NgxConfig {
 	return &NgxConfig{
-		FileName:     filename,
-		commentQueue: &CommentQueue{linkedlistqueue.New()},
-		Upstreams:    make([]*NgxUpstream, 0),
+		FileName:  filename,
+		Upstreams: make([]*NgxUpstream, 0),
 	}
 }

+ 19 - 13
server/router/routers.go

@@ -3,7 +3,6 @@ package router
 import (
 	"bufio"
 	"github.com/0xJacky/Nginx-UI/server/api"
-	"github.com/0xJacky/Nginx-UI/server/settings"
 	"github.com/gin-contrib/static"
 	"github.com/gin-gonic/gin"
 	"net/http"
@@ -34,13 +33,6 @@ func InitRouter() *gin.Engine {
 
 	root := r.Group("/api")
 	{
-
-		root.GET("settings", func(c *gin.Context) {
-			c.JSON(http.StatusOK, gin.H{
-				"demo": settings.ServerSettings.Demo,
-			})
-		})
-
 		root.GET("install", api.InstallLockCheck)
 		root.POST("install", api.InstallNginxUI)
 
@@ -68,27 +60,37 @@ func InitRouter() *gin.Engine {
 			g.POST("ngx/build_config", api.BuildNginxConfig)
 			// Tokenized nginx configuration to NgxConf
 			g.POST("ngx/tokenize_config", api.TokenizeNginxConfig)
+			// Format nginx configuration code
+			g.POST("ngx/format_code", api.FormatNginxConfig)
 
 			g.POST("domain/:name/enable", api.EnableDomain)
 			g.POST("domain/:name/disable", api.DisableDomain)
 			g.DELETE("domain/:name", api.DeleteDomain)
 
 			g.GET("configs", api.GetConfigs)
-			g.GET("config/:name", api.GetConfig)
+			g.GET("config/*name", api.GetConfig)
 			g.POST("config", api.AddConfig)
-			g.POST("config/:name", api.EditConfig)
+			g.POST("config/*name", api.EditConfig)
 
 			//g.GET("backups", api.GetFileBackupList)
 			//g.GET("backup/:id", api.GetFileBackup)
 
 			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/:domain", api.IssueCert)
+			g.GET("cert/issue", api.IssueCert)
 
+			g.GET("certs", api.GetCertList)
+			g.GET("cert/:id", api.GetCert)
+			g.POST("cert", api.AddCert)
+			g.POST("cert/:id", api.ModifyCert)
+			g.DELETE("cert/:id", api.RemoveCert)
 			// Add domain to auto-renew cert list
-			g.POST("cert/:domain", api.AddDomainToAutoCert)
+			g.POST("auto_cert/:domain", api.AddDomainToAutoCert)
 			// Delete domain from auto-renew cert list
-			g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
+			g.DELETE("auto_cert/:domain", api.RemoveDomainFromAutoCert)
 
 			// pty
 			g.GET("pty", api.Pty)
@@ -96,6 +98,10 @@ func InitRouter() *gin.Engine {
 			// Nginx log
 			g.GET("nginx_log", api.NginxLog)
 			g.POST("nginx_log", api.GetNginxLogPage)
+
+			// Settings
+			g.GET("settings", api.GetSettings)
+			g.POST("settings", api.SaveSettings)
 		}
 	}
 

+ 149 - 0
server/service/template.go

@@ -0,0 +1,149 @@
+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{
+				Path: strings.Join(d.GetParameters(), " "),
+			}
+			l.ParseLocation(d, 0)
+			c.NgxServer.Locations = append(c.NgxServer.Locations, l)
+		default:
+			dir := &nginx.NgxDirective{
+				Directive: d.GetName(),
+			}
+			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
+}

+ 13 - 12
server/settings/settings.go

@@ -16,21 +16,21 @@ var (
 )
 
 type Server struct {
-	HttpPort          string
-	RunMode           string
-	WebSocketToken    string
-	JwtSecret         string
-	HTTPChallengePort string
-	Email             string
-	Database          string
-	StartCmd          string
-	Demo              bool
-	PageSize          int
+	HttpPort          string `json:"http_port"`
+	RunMode           string `json:"run_mode"`
+	JwtSecret         string `json:"jwt_secret"`
+	HTTPChallengePort string `json:"http_challenge_port"`
+	Email             string `json:"email"`
+	Database          string `json:"database"`
+	StartCmd          string `json:"start_cmd"`
+	CADir             string `json:"ca_dir"`
+	Demo              bool   `json:"demo"`
+	PageSize          int    `json:"page_size"`
 }
 
 type NginxLog struct {
-	AccessLogPath string
-	ErrorLogPath  string
+	AccessLogPath string `json:"access_log_path"`
+	ErrorLogPath  string `json:"error_log_path"`
 }
 
 var ServerSettings = &Server{
@@ -41,6 +41,7 @@ var ServerSettings = &Server{
 	StartCmd:          "login",
 	Demo:              false,
 	PageSize:          10,
+	CADir:             "",
 }
 
 var NginxLogSettings = &NginxLog{

+ 0 - 42
server/test/ngx_conf_parse_test.go

@@ -1,42 +0,0 @@
-package test
-
-import (
-	"fmt"
-	"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
-	"testing"
-)
-
-func TestNgxConfParse(t *testing.T) {
-	c, err := nginx.ParseNgxConfig("nextcloud_ngx.conf")
-
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	fmt.Println(c.FileName)
-	// directive in root
-	fmt.Println("Upstream")
-	for _, u := range c.Upstreams {
-		fmt.Println("upstream name", u.Name)
-		fmt.Printf("comments\n%v", u.Comments)
-		for _, d := range u.Directives {
-			fmt.Println("u.Directives.d", d)
-		}
-	}
-	fmt.Println("==========================")
-	fmt.Println("Servers")
-	for _, s := range c.Servers {
-		fmt.Printf("comments\n%v", s.Comments)
-		for _, d := range s.Directives {
-			fmt.Println(d)
-		}
-		// locations
-		for _, location := range s.Locations {
-			fmt.Printf("comments\n%v", location.Comments)
-			fmt.Println("path", location.Path)
-			fmt.Println("content", location.Content)
-			fmt.Println("==========================")
-		}
-	}
-
-}

+ 9 - 0
template/block/codeigniter.conf

@@ -0,0 +1,9 @@
+# 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;
+}

+ 13 - 0
template/block/enable-php-8.conf

@@ -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;
+}

+ 9 - 0
template/block/laravel.conf

@@ -0,0 +1,9 @@
+# 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;
+}

+ 23 - 0
template/block/nginx-ui.conf

@@ -0,0 +1,23 @@
+# Nginx UI Template Start
+# Name: Nginx UI
+# Description[en]: Nginx UI Config Template
+# Description[zh_CN]: Nginx UI 配置模板
+# Author: @0xJacky
+# Nginx UI Template End
+
+# Nginx UI Custom Start
+map $http_upgrade $connection_upgrade {
+    default upgrade;
+    '' close;
+}
+# Nginx UI Custom End
+location / {
+        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;
+        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/;
+}

+ 15 - 0
template/block/reverse_proxy.conf

@@ -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;
+ }

+ 26 - 0
template/block/reverse_proxy_ws.conf

@@ -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;
+ }

+ 12 - 0
template/block/wordpress.conf

@@ -0,0 +1,12 @@
+# Nginx UI Template Start
+# Name: WordPress
+# Description[en]: WordPress Config Template
+# Description[zh_CN]: WordPress  配置模板
+# Author: @0xJacky
+# Nginx UI Template End
+location / {
+		try_files $uri $uri/ /index.php?$args;
+}
+
+# Add trailing slash to */wp-admin requests.
+rewrite /wp-admin$ $scheme://$host$uri/ permanent;

+ 12 - 0
template/conf/wordpress.conf

@@ -0,0 +1,12 @@
+# Nginx UI Template Start
+# Name: WordPress
+# Description[en]: WordPress Config Template
+# Description[zh_CN]: WordPress  配置模板
+# Author: @0xJacky
+# Nginx UI Template End
+location / {
+		try_files $uri $uri/ /index.php?$args;
+}
+
+# Add trailing slash to */wp-admin requests.
+rewrite /wp-admin$ $scheme://$host$uri/ permanent;

+ 6 - 0
template/template.go

@@ -0,0 +1,6 @@
+package template
+
+import "embed"
+
+//go:embed conf/* block/*
+var DistFS embed.FS

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä