瀏覽代碼

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

v1.7
Jacky 2 年之前
父節點
當前提交
adb6f888d2
共有 89 個文件被更改,包括 3734 次插入1850 次删除
  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. 二進制
      frontend/src/language/zh_CN/app.mo
  26. 343 137
      frontend/src/language/zh_CN/app.po
  27. 二進制
      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.
 # Customize binary.
 full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
 full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
 # Watch these filename extensions.
 # Watch these filename extensions.
-include_ext = ["go", "tpl", "tmpl", "html"]
+include_ext = ["go", "tpl", "tmpl", "html", "conf"]
 # Ignore these filename extensions or directories.
 # Ignore these filename extensions or directories.
 exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "upload"]
 exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "upload"]
 # Watch these directories if you specified.
 # 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)
 - [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)
 - [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)
 - [Ant Design Vue](https://antdv.com)
 - [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
 - [vue3-gettext](https://github.com/jshmrtn/vue3-gettext)
 - [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
 - [vue3-ace-editor](https://github.com/CarterLi/vue3-ace-editor)
+- [Gonginx](https://github.com/tufanbarisyildirim/gonginx)
 
 
 ## Getting Started
 ## 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']
     ALayoutFooter: typeof import('ant-design-vue/es')['LayoutFooter']
     ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
     ALayoutHeader: typeof import('ant-design-vue/es')['LayoutHeader']
     ALayoutSider: typeof import('ant-design-vue/es')['LayoutSider']
     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']
     AMenu: typeof import('ant-design-vue/es')['Menu']
     AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
     AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
     AModal: typeof import('ant-design-vue/es')['Modal']
     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']
     StdDataEntryComponentsStdPassword: typeof import('./src/components/StdDataEntry/components/StdPassword.vue')['default']
     StdDataEntryComponentsStdSelect: typeof import('./src/components/StdDataEntry/components/StdSelect.vue')['default']
     StdDataEntryComponentsStdSelect: typeof import('./src/components/StdDataEntry/components/StdSelect.vue')['default']
     StdDataEntryComponentsStdSelector: typeof import('./src/components/StdDataEntry/components/StdSelector.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",
     "name": "nginx-ui-frontend-next",
     "private": true,
     "private": true,
-    "version": "1.6.8",
+    "version": "1.7.0",
     "type": "commonjs",
     "type": "commonjs",
     "scripts": {
     "scripts": {
         "dev": "vite",
         "dev": "vite",
@@ -39,7 +39,7 @@
         "less": "^4.1.3",
         "less": "^4.1.3",
         "typescript": "^4.6.4",
         "typescript": "^4.6.4",
         "unplugin-vue-components": "^0.22.9",
         "unplugin-vue-components": "^0.22.9",
-        "vite": "^3.2.3",
+        "vite": "^4.0.3",
         "vite-plugin-html": "^3.2.0",
         "vite-plugin-html": "^3.2.0",
         "vue-tsc": "^1.0.9"
         "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'
 import {dark_mode} from '@/lib/theme'
 
 
 let media = window.matchMedia('(prefers-color-scheme: dark)')
 let media = window.matchMedia('(prefers-color-scheme: dark)')
+
 const callback = (media: { matches: any; }) => {
 const callback = (media: { matches: any; }) => {
     const settings = useSettingsStore()
     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)
 callback(media)
+
 if (typeof media.addEventListener === 'function') {
 if (typeof media.addEventListener === 'function') {
     media.addEventListener('change', callback)
     media.addEventListener('change', callback)
 } else if (typeof media.addListener === 'function') {
 } 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() {
     get_template() {
         return http.get('template')
         return http.get('template')
     }
     }
-    
+
     add_auto_cert(domain: string) {
     add_auto_cert(domain: string) {
-        return http.post('cert/' + domain)
+        return http.post('auto_cert/' + domain)
     }
     }
 
 
     remove_auto_cert(domain: string) {
     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) {
     tokenize_config(content: string) {
         return http.post('/ngx/tokenize_config', {content})
         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 'ace-builds/src-noconflict/theme-monokai'
 import {computed} from 'vue'
 import {computed} from 'vue'
 
 
-const props = defineProps<{
-    content: string
-    defaultHeight?: string
-}>()
+const props = defineProps(['content', 'defaultHeight'])
 
 
 const emit = defineEmits(['update:content'])
 const emit = defineEmits(['update:content'])
 
 
 const value = computed({
 const value = computed({
     get() {
     get() {
-        return props.content
+        return props.content ?? ''
     },
     },
     set(value) {
     set(value) {
         emit('update:content', value)
         emit('update:content', value)
@@ -27,7 +24,7 @@ const value = computed({
         lang="nginx"
         lang="nginx"
         theme="monokai"
         theme="monokai"
         :style="{
         :style="{
-            minHeight: props.defaultHeight || '100vh'
+            minHeight: defaultHeight || '100vh'
         }"/>
         }"/>
 </template>
 </template>
 
 

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

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

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

@@ -10,6 +10,14 @@ import logo from '@/assets/img/logo.png'</script>
 </template>
 </template>
 
 
 <style lang="less" scoped>
 <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 {
 .logo {
     padding: 8px 25px;
     padding: 8px 25px;
     -webkit-box-shadow: 1px 1px 0 0 #e8e8e8;
     -webkit-box-shadow: 1px 1px 0 0 #e8e8e8;
@@ -20,11 +28,6 @@ import logo from '@/assets/img/logo.png'</script>
     overflow: hidden;
     overflow: hidden;
     display: inline-block;
     display: inline-block;
     background-color: #ffffff;
     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 {
     img {
         height: 46px;
         height: 46px;

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

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

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

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

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

@@ -79,28 +79,28 @@ const _selectedKey = computed({
                 {{ M_value }}
                 {{ M_value }}
             </div>
             </div>
             <a-modal
             <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 }}
                 {{ description }}
                 <std-table
                 <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>
             </a-modal>
         </div>
         </div>
@@ -108,6 +108,7 @@ const _selectedKey = computed({
 </template>
 </template>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
+.dark .std-selector-container
 .std-selector-container {
 .std-selector-container {
     height: 39.9px;
     height: 39.9px;
     display: flex;
     display: flex;
@@ -132,15 +133,9 @@ const _selectedKey = computed({
         cursor: pointer;
         cursor: pointer;
         min-width: 180px;
         min-width: 180px;
 
 
-        @media (prefers-color-scheme: dark) {
-            background-color: #1e1f20;
-            border: 1px solid #666666;
-            color: rgba(255, 255, 255, 0.99);
-        }
-
         .value {
         .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"
 "Content-Transfer-Encoding: 8bit\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 "Plural-Forms: nplurals=2; plural=(n != 1);\n"
 
 
-#: src/routes/index.ts:116
+#: src/routes/index.ts:134
 msgid "About"
 msgid "About"
 msgstr "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"
 msgid "Access Logs"
 msgstr ""
 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"
 msgid "Action"
 msgstr "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"
 msgid "Add"
 msgstr ""
 msgstr ""
 
 
@@ -33,47 +34,75 @@ msgstr ""
 msgid "Add Directive Below"
 msgid "Add Directive Below"
 msgstr "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"
 msgid "Add Location"
 msgstr "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"
 msgid "Add Site"
 msgstr "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"
 msgid "Advance Mode"
 msgstr "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
 #, 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?"
 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?"
 msgid "Are you sure you want to remove this directive?"
 msgstr "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
 #, fuzzy
 msgid "Are you sure you want to remove this location?"
 msgid "Are you sure you want to remove this location?"
 msgstr "Are you sure you want to remove this directive?"
 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
 #: src/views/nginx_log/NginxLog.vue:4
 msgid "Auto Refresh"
 msgid "Auto Refresh"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/IssueCert.vue:78
+#: src/views/domain/cert/IssueCert.vue:71
 msgid "Auto-renewal disabled for %{name}"
 msgid "Auto-renewal disabled for %{name}"
 msgstr "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}"
 msgid "Auto-renewal enabled for %{name}"
 msgstr "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"
 msgid "Back"
 msgstr "Back"
 msgstr "Back"
 
 
@@ -86,40 +115,65 @@ msgstr "Back"
 msgid "Base information"
 msgid "Base information"
 msgstr "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"
 msgid "Basic Mode"
 msgstr "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
 #: src/views/other/About.vue:21
 msgid "Build with"
 msgid "Build with"
 msgstr "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"
 msgid "Cancel"
 msgstr "Cancel"
 msgstr "Cancel"
 
 
-#: src/views/domain/cert/CertInfo.vue:24
+#: src/views/domain/cert/CertInfo.vue:19
 msgid "Certificate has expired"
 msgid "Certificate has expired"
 msgstr "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"
 msgid "Certificate is valid"
 msgstr "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"
 msgid "Certificate Status"
 msgstr "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"
 msgid "Comments"
 msgstr "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"
 msgid "Configuration Name"
 msgstr "Configuration Name"
 msgstr "Configuration Name"
 
 
@@ -131,8 +185,9 @@ msgstr "Configurations"
 msgid "Configure SSL"
 msgid "Configure SSL"
 msgstr "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"
 msgid "Content"
 msgstr "Content"
 msgstr "Content"
 
 
@@ -144,7 +199,7 @@ msgstr "CPU Status"
 msgid "CPU:"
 msgid "CPU:"
 msgstr "CPU:"
 msgstr "CPU:"
 
 
-#: src/views/domain/DomainAdd.vue:150
+#: src/views/domain/DomainAdd.vue:149
 msgid "Create Another"
 msgid "Create Another"
 msgstr "Create Another"
 msgstr "Create Another"
 
 
@@ -156,7 +211,21 @@ msgstr "Created at"
 msgid "Creating client facilitates communication with the CA server"
 msgid "Creating client facilitates communication with the CA server"
 msgstr ""
 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"
 msgid "Dashboard"
 msgstr "Dashboard"
 msgstr "Dashboard"
 
 
@@ -164,41 +233,58 @@ msgstr "Dashboard"
 msgid "Database (Optional, default: database)"
 msgid "Database (Optional, default: database)"
 msgstr "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"
 msgid "Delete"
 msgstr ""
 msgstr ""
 
 
-#: src/components/StdDataDisplay/StdTable.vue:120
+#: src/components/StdDataDisplay/StdTable.vue:132
 msgid "Delete ID: %{id}"
 msgid "Delete ID: %{id}"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/DomainList.vue:76
+#: src/views/domain/DomainList.vue:81
 msgid "Delete site: %{site_name}"
 msgid "Delete site: %{site_name}"
 msgstr ""
 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
 #: src/views/other/About.vue:7 src/views/other/About.vue:8
 msgid "Development Mode"
 msgid "Development Mode"
 msgstr "Development Mode"
 msgstr "Development Mode"
 
 
+#: src/views/config/config.ts:20
+msgid "Dir"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:20
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:20
 msgid "Directive"
 msgid "Directive"
 msgstr "Directive"
 msgstr "Directive"
 
 
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:1
 #: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:2
 #: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:2
 msgid "Directives"
 msgid "Directives"
 msgstr "Directives"
 msgstr "Directives"
 
 
-#: src/views/domain/cert/IssueCert.vue:80
+#: src/views/domain/cert/IssueCert.vue:73
 msgid "Disable auto-renewal failed for %{name}"
 msgid "Disable auto-renewal failed for %{name}"
 msgstr "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"
 msgid "Disabled"
 msgstr "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"
 msgid "Disabled successfully"
 msgstr "Disabled successfully"
 msgstr "Disabled successfully"
 
 
@@ -206,19 +292,23 @@ msgstr "Disabled successfully"
 msgid "Disk IO"
 msgid "Disk IO"
 msgstr "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"
 msgid "Domain Config Created Successfully"
 msgstr "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}"
 msgid "Edit %{n}"
 msgstr "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"
 msgid "Edit Configuration"
 msgstr "Edit Configuration"
 msgstr "Edit Configuration"
 
 
-#: src/routes/index.ts:59
+#: src/routes/index.ts:61
 msgid "Edit Site"
 msgid "Edit Site"
 msgstr "Edit Site"
 msgstr "Edit Site"
 
 
@@ -226,11 +316,11 @@ msgstr "Edit Site"
 msgid "Email (*)"
 msgid "Email (*)"
 msgstr "Email (*)"
 msgstr "Email (*)"
 
 
-#: src/views/domain/cert/IssueCert.vue:74
+#: src/views/domain/cert/IssueCert.vue:67
 msgid "Enable auto-renewal failed for %{name}"
 msgid "Enable auto-renewal failed for %{name}"
 msgstr "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"
 msgid "Enable failed"
 msgstr "Enable failed"
 msgstr "Enable failed"
 
 
@@ -238,39 +328,43 @@ msgstr "Enable failed"
 msgid "Enable TLS"
 msgid "Enable TLS"
 msgstr "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"
 msgid "Enabled"
 msgstr "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"
 msgid "Enabled successfully"
 msgstr "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"
 msgid "Encrypt website with Let's Encrypt"
 msgstr "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"
 msgid "Error Logs"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/CertInfo.vue:17
+#: src/views/domain/cert/CertInfo.vue:12
 msgid "Expiration Date: %{date}"
 msgid "Expiration Date: %{date}"
 msgstr "Expiration Date: %{date}"
 msgstr "Expiration Date: %{date}"
 
 
 #: src/components/StdDataDisplay/StdTable.vue:12
 #: 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"
 msgid "Export"
 msgstr ""
 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}"
 msgid "Failed to disable %{msg}"
 msgstr "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}"
 msgid "Failed to enable %{msg}"
 msgstr "Failed to enable %{msg}"
 msgstr "Failed to enable %{msg}"
 
 
@@ -278,6 +372,10 @@ msgstr "Failed to enable %{msg}"
 msgid "Failed to get certificate information"
 msgid "Failed to get certificate information"
 msgstr ""
 msgstr ""
 
 
+#: src/views/config/config.ts:22
+msgid "File"
+msgstr ""
+
 #: src/views/other/Error.vue:3 src/views/other/Error.vue:4
 #: src/views/other/Error.vue:3 src/views/other/Error.vue:4
 msgid "File Not Found"
 msgid "File Not Found"
 msgstr "File Not Found"
 msgstr "File Not Found"
@@ -290,7 +388,21 @@ msgstr ""
 msgid "Finished"
 msgid "Finished"
 msgstr "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"
 msgid "Generate"
 msgstr ""
 msgstr ""
 
 
@@ -298,15 +410,23 @@ msgstr ""
 msgid "Generating private key for registering account"
 msgid "Generating private key for registering account"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/IssueCert.vue:103
+#: src/views/domain/cert/IssueCert.vue:96
 msgid "Getting the certificate, please wait..."
 msgid "Getting the certificate, please wait..."
 msgstr "Getting the certificate, please wait..."
 msgstr "Getting the certificate, please wait..."
 
 
-#: src/routes/index.ts:20
+#: src/routes/index.ts:22
 msgid "Home"
 msgid "Home"
 msgstr "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"
 msgid "Install"
 msgstr "Install"
 msgstr "Install"
 
 
@@ -315,7 +435,7 @@ msgstr "Install"
 msgid "Install successfully"
 msgid "Install successfully"
 msgstr "Enabled successfully"
 msgstr "Enabled successfully"
 
 
-#: src/views/domain/cert/CertInfo.vue:15
+#: src/views/domain/cert/CertInfo.vue:10
 msgid "Intermediate Certification Authorities: %{issuer}"
 msgid "Intermediate Certification Authorities: %{issuer}"
 msgstr "Intermediate Certification Authorities: %{issuer}"
 msgstr "Intermediate Certification Authorities: %{issuer}"
 
 
@@ -324,23 +444,34 @@ msgstr "Intermediate Certification Authorities: %{issuer}"
 msgid "Issued certificate successfully"
 msgid "Issued certificate successfully"
 msgstr "Enabled successfully"
 msgstr "Enabled successfully"
 
 
+#: src/views/preference/Preference.vue:11
+msgid "Jwt Secret"
+msgstr ""
+
 #: src/views/user/User.vue:26
 #: src/views/user/User.vue:26
 msgid "Leave blank for no change"
 msgid "Leave blank for no change"
 msgstr "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
 #: src/views/dashboard/DashBoard.vue:141
 msgid "Load Averages:"
 msgid "Load Averages:"
 msgstr "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"
 msgid "Location"
 msgstr "Location"
 msgstr "Location"
 
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:39
+#: src/views/domain/ngx_conf/LocationEditor.vue:40
 msgid "Locations"
 msgid "Locations"
 msgstr "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"
 msgid "Login"
 msgstr "Login"
 msgstr "Login"
 
 
@@ -352,7 +483,7 @@ msgstr "Login successful"
 msgid "Logout successful"
 msgid "Logout successful"
 msgstr "Logout successful"
 msgstr "Logout successful"
 
 
-#: src/views/domain/cert/IssueCert.vue:226
+#: src/views/domain/cert/IssueCert.vue:211
 msgid ""
 msgid ""
 "Make sure you have configured a reverse proxy for .well-known directory to "
 "Make sure you have configured a reverse proxy for .well-known directory to "
 "HTTPChallengePort (default: 9180) before getting the certificate."
 "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 "
 "Make sure you have configured a reverse proxy for .well-known directory to "
 "HTTPChallengePort (default: 9180) before getting the certificate."
 "HTTPChallengePort (default: 9180) before getting the certificate."
 
 
-#: src/routes/index.ts:68
+#: src/routes/index.ts:70
 msgid "Manage Configs"
 msgid "Manage Configs"
 msgstr "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"
 msgid "Manage Sites"
 msgstr "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"
 msgid "Manage Users"
 msgstr "Manage Users"
 msgstr "Manage Users"
 
 
@@ -380,21 +511,28 @@ msgstr "Memory"
 msgid "Memory and Storage"
 msgid "Memory and Storage"
 msgstr "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
 #, fuzzy
 msgid "Modify"
 msgid "Modify"
 msgstr "Modify Config"
 msgstr "Modify Config"
 
 
-#: src/views/domain/DomainAdd.vue:147
+#: src/views/domain/DomainAdd.vue:146
 msgid "Modify Config"
 msgid "Modify Config"
 msgstr "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"
 msgid "Name"
 msgstr "Name"
 msgstr "Name"
 
 
@@ -414,45 +552,50 @@ msgstr "Network Total Receive"
 msgid "Network Total Send"
 msgid "Network Total Send"
 msgstr "Network Total Send"
 msgstr "Network Total Send"
 
 
-#: src/views/domain/DomainAdd.vue:137
+#: src/views/domain/DomainAdd.vue:136
 msgid "Next"
 msgid "Next"
 msgstr "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"
 msgid "Nginx Log"
 msgstr ""
 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"
 msgid "No"
 msgstr "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"
 msgid "Not Found"
 msgstr "Not Found"
 msgstr "Not Found"
 
 
-#: src/views/domain/cert/CertInfo.vue:19
+#: src/views/domain/cert/CertInfo.vue:14
 msgid "Not Valid Before: %{date}"
 msgid "Not Valid Before: %{date}"
 msgstr "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 ""
 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
 #: src/language/constants.ts:16 src/views/domain/cert/IssueCert.vue:3
 msgid "Obtaining certificate"
 msgid "Obtaining certificate"
 msgstr ""
 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"
 msgid "OK"
 msgstr ""
 msgstr ""
 
 
@@ -472,8 +615,8 @@ msgstr "Password"
 msgid "Password (*)"
 msgid "Password (*)"
 msgstr "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"
 msgid "Path"
 msgstr "Path"
 msgstr "Path"
 
 
@@ -489,6 +632,10 @@ msgstr "Please input your password!"
 msgid "Please input your username!"
 msgid "Please input your username!"
 msgstr "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
 #: src/language/constants.ts:12
 #, fuzzy
 #, fuzzy
 msgid "Preparing lego configurations"
 msgid "Preparing lego configurations"
@@ -522,35 +669,56 @@ msgstr ""
 msgid "Reloading nginx"
 msgid "Reloading nginx"
 msgstr ""
 msgstr ""
 
 
+#: src/components/StdDataDisplay/StdTable.vue:10
 #: src/components/StdDataDisplay/StdTable.vue:15
 #: src/components/StdDataDisplay/StdTable.vue:15
+#: src/components/StdDataDisplay/StdTable.vue:9
 msgid "Reset"
 msgid "Reset"
 msgstr ""
 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"
 msgid "Save"
 msgstr "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:35
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:36
 msgid "Save Directive"
 msgid "Save Directive"
 msgstr "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}"
 msgid "Save error %{msg}"
 msgstr "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
 #, fuzzy
 msgid "Save Successfully"
 msgid "Save Successfully"
 msgstr "Saved 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"
 msgid "Saved successfully"
 msgstr "Saved successfully"
 msgstr "Saved successfully"
 
 
-#: src/components/StdDataEntry/compontents/StdSelector.vue:13
+#: src/components/StdDataEntry/components/StdSelector.vue:13
 msgid "Selector"
 msgid "Selector"
 msgstr ""
 msgstr ""
 
 
@@ -558,12 +726,14 @@ msgstr ""
 msgid "Send"
 msgid "Send"
 msgstr "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"
 msgid "Server error"
 msgstr "Server error"
 msgstr "Server error"
 
 
@@ -571,33 +741,49 @@ msgstr "Server error"
 msgid "Server Info"
 msgid "Server Info"
 msgstr "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"
 msgid "server_name not found in directives"
 msgstr "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"
 msgid "server_name parameter is required"
 msgstr "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
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:7
 msgid "Single Directive"
 msgid "Single Directive"
 msgstr "Single Directive"
 msgstr "Single Directive"
 
 
-#: src/routes/index.ts:107
+#: src/routes/index.ts:117
 #, fuzzy
 #, fuzzy
 msgid "Site Logs"
 msgid "Site Logs"
 msgstr "Sites List"
 msgstr "Sites List"
 
 
-#: src/routes/index.ts:51
+#: src/routes/index.ts:53
 msgid "Sites List"
 msgid "Sites List"
 msgstr "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"
 msgid "Status"
 msgstr "Status"
 msgstr "Status"
 
 
@@ -605,7 +791,7 @@ msgstr "Status"
 msgid "Storage"
 msgid "Storage"
 msgstr "Storage"
 msgstr "Storage"
 
 
-#: src/views/domain/cert/CertInfo.vue:16
+#: src/views/domain/cert/CertInfo.vue:11
 msgid "Subject Name: %{name}"
 msgid "Subject Name: %{name}"
 msgstr "Subject Name: %{name}"
 msgstr "Subject Name: %{name}"
 
 
@@ -618,11 +804,15 @@ msgstr "Swap"
 msgid "Table"
 msgid "Table"
 msgstr "Enabled"
 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"
 msgid "Terminal"
 msgstr "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 ""
 msgid ""
 "The certificate for the domain will be checked every hour, and will be "
 "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."
 "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}"
 msgid "The filename cannot contain the following characters: %{c}"
 msgstr "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
 #: src/language/constants.ts:6
 msgid "The username or password is incorrect"
 msgid "The username or password is incorrect"
 msgstr ""
 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"
 msgid "Updated at"
 msgstr "Updated at"
 msgstr "Updated at"
 
 
+#: src/components/StdDataDisplay/StdTable.vue:461
+#, fuzzy
+msgid "Updated successfully"
+msgstr "Saved successfully"
+
 #: src/views/dashboard/DashBoard.vue:137
 #: src/views/dashboard/DashBoard.vue:137
 msgid "Uptime:"
 msgid "Uptime:"
 msgstr "Uptime:"
 msgstr "Uptime:"
@@ -659,7 +871,13 @@ msgstr "Username (*)"
 msgid "Using HTTP01 challenge provider"
 msgid "Using HTTP01 challenge provider"
 msgstr ""
 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"
 msgid "Warning"
 msgstr "Warning"
 msgstr "Warning"
 
 
@@ -676,8 +894,8 @@ msgstr ""
 msgid "Writing certificate to disk"
 msgid "Writing certificate to disk"
 msgstr ""
 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"
 msgid "Yes"
 msgstr "Yes"
 msgstr "Yes"
 
 
@@ -686,6 +904,13 @@ msgctxt "Project"
 msgid "License"
 msgid "License"
 msgstr "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"
 #~ msgid "404 Not Found"
 #~ msgstr "404 Not Found"
 #~ msgstr "404 Not Found"
 
 

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

@@ -2,23 +2,25 @@ msgid ""
 msgstr ""
 msgstr ""
 "Content-Type: text/plain; charset=UTF-8\n"
 "Content-Type: text/plain; charset=UTF-8\n"
 
 
-#: src/routes/index.ts:116
+#: src/routes/index.ts:134
 msgid "About"
 msgid "About"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:99
+#: src/routes/index.ts:109
 #: src/views/domain/ngx_conf/LogEntry.vue:64
 #: src/views/domain/ngx_conf/LogEntry.vue:64
 msgid "Access Logs"
 msgid "Access Logs"
 msgstr ""
 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
 #: src/views/user/User.vue:43
 msgid "Action"
 msgid "Action"
 msgstr ""
 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"
 msgid "Add"
 msgstr ""
 msgstr ""
 
 
@@ -28,47 +30,78 @@ msgstr ""
 msgid "Add Directive Below"
 msgid "Add Directive Below"
 msgstr ""
 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"
 msgid "Add Location"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:55
+#: src/routes/index.ts:57
 #: src/views/domain/DomainAdd.vue:2
 #: src/views/domain/DomainAdd.vue:2
 msgid "Add Site"
 msgid "Add Site"
 msgstr ""
 msgstr ""
 
 
+#: src/views/domain/DomainEdit.vue:18
 #: src/views/domain/DomainEdit.vue:19
 #: src/views/domain/DomainEdit.vue:19
 msgid "Advance Mode"
 msgid "Advance Mode"
 msgstr ""
 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 ""
 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?"
 msgid "Are you sure you want to remove this directive?"
 msgstr ""
 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?"
 msgid "Are you sure you want to remove this location?"
 msgstr ""
 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
 #: src/views/nginx_log/NginxLog.vue:4
 msgid "Auto Refresh"
 msgid "Auto Refresh"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/IssueCert.vue:78
+#: src/views/domain/cert/IssueCert.vue:71
 msgid "Auto-renewal disabled for %{name}"
 msgid "Auto-renewal disabled for %{name}"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/IssueCert.vue:72
+#: src/views/domain/cert/IssueCert.vue:65
 msgid "Auto-renewal enabled for %{name}"
 msgid "Auto-renewal enabled for %{name}"
 msgstr ""
 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"
 msgid "Back"
 msgstr ""
 msgstr ""
 
 
@@ -80,40 +113,64 @@ msgstr ""
 msgid "Base information"
 msgid "Base information"
 msgstr ""
 msgstr ""
 
 
+#: src/views/domain/DomainEdit.vue:21
 #: src/views/domain/DomainEdit.vue:22
 #: src/views/domain/DomainEdit.vue:22
 msgid "Basic Mode"
 msgid "Basic Mode"
 msgstr ""
 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
 #: src/views/other/About.vue:21
 msgid "Build with"
 msgid "Build with"
 msgstr ""
 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"
 msgid "Cancel"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/CertInfo.vue:24
+#: src/views/domain/cert/CertInfo.vue:19
 msgid "Certificate has expired"
 msgid "Certificate has expired"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/CertInfo.vue:28
+#: src/views/domain/cert/CertInfo.vue:23
 msgid "Certificate is valid"
 msgid "Certificate is valid"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/CertInfo.vue:14
+#: src/views/cert/Cert.vue:12
+#: src/views/domain/cert/Cert.vue:35
 msgid "Certificate Status"
 msgid "Certificate Status"
 msgstr ""
 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"
 msgid "Comments"
 msgstr ""
 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"
 msgid "Configuration Name"
 msgstr ""
 msgstr ""
 
 
@@ -125,8 +182,9 @@ msgstr ""
 msgid "Configure SSL"
 msgid "Configure SSL"
 msgstr ""
 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"
 msgid "Content"
 msgstr ""
 msgstr ""
 
 
@@ -138,7 +196,7 @@ msgstr ""
 msgid "CPU:"
 msgid "CPU:"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/DomainAdd.vue:150
+#: src/views/domain/DomainAdd.vue:149
 msgid "Create Another"
 msgid "Create Another"
 msgstr ""
 msgstr ""
 
 
@@ -150,7 +208,21 @@ msgstr ""
 msgid "Creating client facilitates communication with the CA server"
 msgid "Creating client facilitates communication with the CA server"
 msgstr ""
 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"
 msgid "Dashboard"
 msgstr ""
 msgstr ""
 
 
@@ -158,44 +230,64 @@ msgstr ""
 msgid "Database (Optional, default: database)"
 msgid "Database (Optional, default: database)"
 msgstr ""
 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"
 msgid "Delete"
 msgstr ""
 msgstr ""
 
 
-#: src/components/StdDataDisplay/StdTable.vue:120
+#: src/components/StdDataDisplay/StdTable.vue:132
 msgid "Delete ID: %{id}"
 msgid "Delete ID: %{id}"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/DomainList.vue:76
+#: src/views/domain/DomainList.vue:81
 msgid "Delete site: %{site_name}"
 msgid "Delete site: %{site_name}"
 msgstr ""
 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:7
 #: src/views/other/About.vue:8
 #: src/views/other/About.vue:8
 msgid "Development Mode"
 msgid "Development Mode"
 msgstr ""
 msgstr ""
 
 
+#: src/views/config/config.ts:20
+msgid "Dir"
+msgstr ""
+
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:20
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:20
 msgid "Directive"
 msgid "Directive"
 msgstr ""
 msgstr ""
 
 
+#: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:1
 #: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:2
 #: src/views/domain/ngx_conf/directive/DirectiveEditor.vue:2
 msgid "Directives"
 msgid "Directives"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/IssueCert.vue:80
+#: src/views/domain/cert/IssueCert.vue:73
 msgid "Disable auto-renewal failed for %{name}"
 msgid "Disable auto-renewal failed for %{name}"
 msgstr ""
 msgstr ""
 
 
+#: src/views/cert/Cert.vue:51
 #: src/views/domain/DomainEdit.vue:10
 #: 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"
 msgid "Disabled"
 msgstr ""
 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"
 msgid "Disabled successfully"
 msgstr ""
 msgstr ""
 
 
@@ -203,20 +295,25 @@ msgstr ""
 msgid "Disk IO"
 msgid "Disk IO"
 msgstr ""
 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"
 msgid "Domain Config Created Successfully"
 msgstr ""
 msgstr ""
 
 
+#: src/views/domain/DomainEdit.vue:4
 #: src/views/domain/DomainEdit.vue:5
 #: src/views/domain/DomainEdit.vue:5
 msgid "Edit %{n}"
 msgid "Edit %{n}"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:77
+#: src/routes/index.ts:79
 #: src/views/config/ConfigEdit.vue:2
 #: src/views/config/ConfigEdit.vue:2
 msgid "Edit Configuration"
 msgid "Edit Configuration"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:59
+#: src/routes/index.ts:61
 msgid "Edit Site"
 msgid "Edit Site"
 msgstr ""
 msgstr ""
 
 
@@ -224,11 +321,11 @@ msgstr ""
 msgid "Email (*)"
 msgid "Email (*)"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/IssueCert.vue:74
+#: src/views/domain/cert/IssueCert.vue:67
 msgid "Enable auto-renewal failed for %{name}"
 msgid "Enable auto-renewal failed for %{name}"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/DomainAdd.vue:50
+#: src/views/domain/DomainAdd.vue:51
 msgid "Enable failed"
 msgid "Enable failed"
 msgstr ""
 msgstr ""
 
 
@@ -236,45 +333,51 @@ msgstr ""
 msgid "Enable TLS"
 msgid "Enable TLS"
 msgstr ""
 msgstr ""
 
 
+#: src/views/cert/Cert.vue:48
 #: src/views/domain/DomainEdit.vue:33
 #: src/views/domain/DomainEdit.vue:33
+#: src/views/domain/DomainEdit.vue:6
 #: src/views/domain/DomainEdit.vue:7
 #: 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: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"
 msgid "Enabled"
 msgstr ""
 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"
 msgid "Enabled successfully"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/IssueCert.vue:17
+#: src/views/domain/cert/IssueCert.vue:18
 msgid "Encrypt website with Let's Encrypt"
 msgid "Encrypt website with Let's Encrypt"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:103
+#: src/routes/index.ts:113
 #: src/views/domain/ngx_conf/LogEntry.vue:68
 #: src/views/domain/ngx_conf/LogEntry.vue:68
 msgid "Error Logs"
 msgid "Error Logs"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/CertInfo.vue:17
+#: src/views/domain/cert/CertInfo.vue:12
 msgid "Expiration Date: %{date}"
 msgid "Expiration Date: %{date}"
 msgstr ""
 msgstr ""
 
 
 #: src/components/StdDataDisplay/StdTable.vue:12
 #: 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"
 msgid "Export"
 msgstr ""
 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}"
 msgid "Failed to disable %{msg}"
 msgstr ""
 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}"
 msgid "Failed to enable %{msg}"
 msgstr ""
 msgstr ""
 
 
@@ -282,6 +385,10 @@ msgstr ""
 msgid "Failed to get certificate information"
 msgid "Failed to get certificate information"
 msgstr ""
 msgstr ""
 
 
+#: src/views/config/config.ts:22
+msgid "File"
+msgstr ""
+
 #: src/views/other/Error.vue:3
 #: src/views/other/Error.vue:3
 #: src/views/other/Error.vue:4
 #: src/views/other/Error.vue:4
 msgid "File Not Found"
 msgid "File Not Found"
@@ -296,7 +403,19 @@ msgstr ""
 msgid "Finished"
 msgid "Finished"
 msgstr ""
 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"
 msgid "Generate"
 msgstr ""
 msgstr ""
 
 
@@ -304,15 +423,23 @@ msgstr ""
 msgid "Generating private key for registering account"
 msgid "Generating private key for registering account"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/IssueCert.vue:103
+#: src/views/domain/cert/IssueCert.vue:96
 msgid "Getting the certificate, please wait..."
 msgid "Getting the certificate, please wait..."
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:20
+#: src/routes/index.ts:22
 msgid "Home"
 msgid "Home"
 msgstr ""
 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
 #: src/views/other/Install.vue:128
 msgid "Install"
 msgid "Install"
 msgstr ""
 msgstr ""
@@ -321,7 +448,7 @@ msgstr ""
 msgid "Install successfully"
 msgid "Install successfully"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/CertInfo.vue:15
+#: src/views/domain/cert/CertInfo.vue:10
 msgid "Intermediate Certification Authorities: %{issuer}"
 msgid "Intermediate Certification Authorities: %{issuer}"
 msgstr ""
 msgstr ""
 
 
@@ -329,23 +456,34 @@ msgstr ""
 msgid "Issued certificate successfully"
 msgid "Issued certificate successfully"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/Preference.vue:11
+msgid "Jwt Secret"
+msgstr ""
+
 #: src/views/user/User.vue:26
 #: src/views/user/User.vue:26
 msgid "Leave blank for no change"
 msgid "Leave blank for no change"
 msgstr ""
 msgstr ""
 
 
+#: src/views/preference/Preference.vue:25
+#: src/views/preference/Preference.vue:26
+msgid "Light"
+msgstr ""
+
 #: src/views/dashboard/DashBoard.vue:141
 #: src/views/dashboard/DashBoard.vue:141
 msgid "Load Averages:"
 msgid "Load Averages:"
 msgstr ""
 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"
 msgid "Location"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/ngx_conf/LocationEditor.vue:39
+#: src/views/domain/ngx_conf/LocationEditor.vue:40
 msgid "Locations"
 msgid "Locations"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:132
+#: src/routes/index.ts:150
 #: src/views/other/Login.vue:103
 #: src/views/other/Login.vue:103
 msgid "Login"
 msgid "Login"
 msgstr ""
 msgstr ""
@@ -358,20 +496,20 @@ msgstr ""
 msgid "Logout successful"
 msgid "Logout successful"
 msgstr ""
 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."
 msgid "Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate."
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:68
+#: src/routes/index.ts:70
 msgid "Manage Configs"
 msgid "Manage Configs"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:43
+#: src/routes/index.ts:45
 #: src/views/domain/DomainList.vue:2
 #: src/views/domain/DomainList.vue:2
 msgid "Manage Sites"
 msgid "Manage Sites"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:35
+#: src/routes/index.ts:37
 #: src/views/user/User.vue:2
 #: src/views/user/User.vue:2
 msgid "Manage Users"
 msgid "Manage Users"
 msgstr ""
 msgstr ""
@@ -384,21 +522,28 @@ msgstr ""
 msgid "Memory and Storage"
 msgid "Memory and Storage"
 msgstr ""
 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"
 msgid "Modify"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/DomainAdd.vue:147
+#: src/views/domain/DomainAdd.vue:146
 msgid "Modify Config"
 msgid "Modify Config"
 msgstr ""
 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"
 msgid "Name"
 msgstr ""
 msgstr ""
 
 
@@ -418,33 +563,41 @@ msgstr ""
 msgid "Network Total Send"
 msgid "Network Total Send"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/DomainAdd.vue:137
+#: src/views/domain/DomainAdd.vue:136
 msgid "Next"
 msgid "Next"
 msgstr ""
 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
 #: src/views/nginx_log/NginxLog.vue:2
 msgid "Nginx Log"
 msgid "Nginx Log"
 msgstr ""
 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"
 msgid "No"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:138
-#: src/routes/index.ts:140
+#: src/routes/index.ts:156
+#: src/routes/index.ts:158
 msgid "Not Found"
 msgid "Not Found"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/CertInfo.vue:19
+#: src/views/domain/cert/CertInfo.vue:14
 msgid "Not Valid Before: %{date}"
 msgid "Not Valid Before: %{date}"
 msgstr ""
 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 ""
 msgstr ""
 
 
 #: src/language/constants.ts:16
 #: src/language/constants.ts:16
@@ -452,10 +605,11 @@ msgstr ""
 msgid "Obtaining certificate"
 msgid "Obtaining certificate"
 msgstr ""
 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"
 msgid "OK"
 msgstr ""
 msgstr ""
 
 
@@ -476,8 +630,8 @@ msgstr ""
 msgid "Password (*)"
 msgid "Password (*)"
 msgstr ""
 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"
 msgid "Path"
 msgstr ""
 msgstr ""
 
 
@@ -495,6 +649,11 @@ msgstr ""
 msgid "Please input your username!"
 msgid "Please input your username!"
 msgstr ""
 msgstr ""
 
 
+#: src/routes/index.ts:126
+#: src/views/preference/Preference.vue:2
+msgid "Preference"
+msgstr ""
+
 #: src/language/constants.ts:12
 #: src/language/constants.ts:12
 msgid "Preparing lego configurations"
 msgid "Preparing lego configurations"
 msgstr ""
 msgstr ""
@@ -528,37 +687,56 @@ msgstr ""
 msgid "Reloading nginx"
 msgid "Reloading nginx"
 msgstr ""
 msgstr ""
 
 
+#: src/components/StdDataDisplay/StdTable.vue:10
 #: src/components/StdDataDisplay/StdTable.vue:15
 #: src/components/StdDataDisplay/StdTable.vue:15
+#: src/components/StdDataDisplay/StdTable.vue:9
 msgid "Reset"
 msgid "Reset"
 msgstr ""
 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"
 msgid "Save"
 msgstr ""
 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:35
+#: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:36
 msgid "Save Directive"
 msgid "Save Directive"
 msgstr ""
 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}"
 msgid "Save error %{msg}"
 msgstr ""
 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"
 msgid "Save Successfully"
 msgstr ""
 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"
 msgid "Saved successfully"
 msgstr ""
 msgstr ""
 
 
-#: src/components/StdDataEntry/compontents/StdSelector.vue:13
+#: src/components/StdDataEntry/components/StdSelector.vue:13
 msgid "Selector"
 msgid "Selector"
 msgstr ""
 msgstr ""
 
 
@@ -567,15 +745,18 @@ msgstr ""
 msgid "Send"
 msgid "Send"
 msgstr ""
 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/other/Install.vue:71
+#: src/views/preference/Preference.vue:41
 msgid "Server error"
 msgid "Server error"
 msgstr ""
 msgstr ""
 
 
@@ -583,33 +764,45 @@ msgstr ""
 msgid "Server Info"
 msgid "Server Info"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/IssueCert.vue:29
+#: src/views/domain/cert/IssueCert.vue:30
 msgid "server_name not found in directives"
 msgid "server_name not found in directives"
 msgstr ""
 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"
 msgid "server_name parameter is required"
 msgstr ""
 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
 #: src/views/domain/ngx_conf/directive/DirectiveAdd.vue:7
 msgid "Single Directive"
 msgid "Single Directive"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:107
+#: src/routes/index.ts:117
 msgid "Site Logs"
 msgid "Site Logs"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:51
+#: src/routes/index.ts:53
 msgid "Sites List"
 msgid "Sites List"
 msgstr ""
 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"
 msgid "Status"
 msgstr ""
 msgstr ""
 
 
@@ -617,7 +810,7 @@ msgstr ""
 msgid "Storage"
 msgid "Storage"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/cert/CertInfo.vue:16
+#: src/views/domain/cert/CertInfo.vue:11
 msgid "Subject Name: %{name}"
 msgid "Subject Name: %{name}"
 msgstr ""
 msgstr ""
 
 
@@ -629,12 +822,16 @@ msgstr ""
 msgid "Table"
 msgid "Table"
 msgstr ""
 msgstr ""
 
 
-#: src/routes/index.ts:85
+#: src/routes/index.ts:95
 #: src/views/pty/Terminal.vue:2
 #: src/views/pty/Terminal.vue:2
 msgid "Terminal"
 msgid "Terminal"
 msgstr ""
 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."
 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 ""
 msgstr ""
 
 
@@ -642,16 +839,33 @@ msgstr ""
 msgid "The filename cannot contain the following characters: %{c}"
 msgid "The filename cannot contain the following characters: %{c}"
 msgstr ""
 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
 #: src/language/constants.ts:6
 msgid "The username or password is incorrect"
 msgid "The username or password is incorrect"
 msgstr ""
 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
 #: src/views/user/User.vue:37
 msgid "Updated at"
 msgid "Updated at"
 msgstr ""
 msgstr ""
 
 
+#: src/components/StdDataDisplay/StdTable.vue:461
+msgid "Updated successfully"
+msgstr ""
+
 #: src/views/dashboard/DashBoard.vue:137
 #: src/views/dashboard/DashBoard.vue:137
 msgid "Uptime:"
 msgid "Uptime:"
 msgstr ""
 msgstr ""
@@ -669,8 +883,14 @@ msgstr ""
 msgid "Using HTTP01 challenge provider"
 msgid "Using HTTP01 challenge provider"
 msgstr ""
 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"
 msgid "Warning"
 msgstr ""
 msgstr ""
 
 
@@ -688,8 +908,8 @@ msgstr ""
 msgid "Writing certificate to disk"
 msgid "Writing certificate to disk"
 msgstr ""
 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"
 msgid "Yes"
 msgstr ""
 msgstr ""
 
 

File diff suppressed because it is too large
+ 0 - 0
frontend/src/language/translations.json


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

二進制
frontend/src/language/zh_TW/app.mo


File diff suppressed because it is too large
+ 391 - 178
frontend/src/language/zh_TW/app.po


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

@@ -39,7 +39,7 @@ const lang = computed(() => {
 
 
 </script>
 </script>
 <template>
 <template>
-    <a-config-provider :locale="lang">
+    <a-config-provider :locale="lang" :autoInsertSpaceInButton="false">
         <a-layout style="min-height: 100%;">
         <a-layout style="min-height: 100%;">
             <div class="drawer-sidebar">
             <div class="drawer-sidebar">
                 <a-drawer
                 <a-drawer
@@ -122,37 +122,36 @@ const lang = computed(() => {
 </style>
 </style>
 
 
 <style lang="less">
 <style lang="less">
-@dark: ~"(prefers-color-scheme: dark)";
-
 body {
 body {
     overflow: unset !important;
     overflow: unset !important;
 }
 }
 
 
-@media @dark {
+.dark {
     h1, h2, h3, h4, h5, h6, p {
     h1, h2, h3, h4, h5, h6, p {
         color: #fafafa !important;
         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;
         background-color: #1f1f1f !important;
     }
     }
-}
 
 
-.ant-card {
-    @media @dark {
+    .ant-card {
         background-color: #1f1f1f !important;
         background-color: #1f1f1f !important;
     }
     }
-}
 
 
-.ant-layout-sider {
-    background-color: #ffffff;
-    @media @dark {
+    .ant-layout-sider {
         background-color: rgb(20, 20, 20) !important;
         background-color: rgb(20, 20, 20) !important;
+
         .ant-layout-sider-trigger {
         .ant-layout-sider-trigger {
             background-color: rgb(20, 20, 20) !important;
             background-color: rgb(20, 20, 20) !important;
         }
         }
@@ -160,8 +159,25 @@ body {
         .ant-menu {
         .ant-menu {
             border-right: 0 !important;
             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 {
     &.ant-layout-sider-has-trigger {
         padding-bottom: 0;
         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 {
 .ant-table-small {
     font-size: 13px;
     font-size: 13px;

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

@@ -55,7 +55,7 @@ function logout() {
     }
     }
 }
 }
 
 
-@media (prefers-color-scheme: dark) {
+.dark {
     .header {
     .header {
         box-shadow: 1px 1px 0 0 #404040;
         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) => {
 export const dark_mode = async (enabled: Boolean) => {
+    document.body.setAttribute('class', enabled ? 'dark' : 'light')
     if (enabled) {
     if (enabled) {
         changeTheme((await import('@/dark.less?inline')).default)
         changeTheme((await import('@/dark.less?inline')).default)
         changeCss('--page-bg-color', '#141414')
         changeCss('--page-bg-color', '#141414')

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

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

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

@@ -9,7 +9,9 @@ import {
     HomeOutlined,
     HomeOutlined,
     InfoCircleOutlined,
     InfoCircleOutlined,
     UserOutlined,
     UserOutlined,
-    FileTextOutlined
+    FileTextOutlined,
+    SettingOutlined,
+    SafetyCertificateOutlined
 } from '@ant-design/icons-vue'
 } from '@ant-design/icons-vue'
 
 
 const {$gettext} = gettext
 const {$gettext} = gettext
@@ -36,7 +38,7 @@ export const routes = [
                 component: () => import('@/views/user/User.vue'),
                 component: () => import('@/views/user/User.vue'),
                 meta: {
                 meta: {
                     icon: UserOutlined
                     icon: UserOutlined
-                },
+                }
             },
             },
             {
             {
                 path: 'domain',
                 path: 'domain',
@@ -49,11 +51,11 @@ export const routes = [
                 children: [{
                 children: [{
                     path: 'list',
                     path: 'list',
                     name: () => $gettext('Sites List'),
                     name: () => $gettext('Sites List'),
-                    component: () => import('@/views/domain/DomainList.vue'),
+                    component: () => import('@/views/domain/DomainList.vue')
                 }, {
                 }, {
                     path: 'add',
                     path: 'add',
                     name: () => $gettext('Add Site'),
                     name: () => $gettext('Add Site'),
-                    component: () => import('@/views/domain/DomainAdd.vue'),
+                    component: () => import('@/views/domain/DomainAdd.vue')
                 }, {
                 }, {
                     path: ':name',
                     path: ':name',
                     name: () => $gettext('Edit Site'),
                     name: () => $gettext('Edit Site'),
@@ -61,7 +63,7 @@ export const routes = [
                     meta: {
                     meta: {
                         hiddenInSidebar: true
                         hiddenInSidebar: true
                     }
                     }
-                },]
+                }]
             },
             },
             {
             {
                 path: 'config',
                 path: 'config',
@@ -73,12 +75,20 @@ export const routes = [
                 }
                 }
             },
             },
             {
             {
-                path: 'config/:name',
+                path: 'config/:name+/edit',
                 name: () => $gettext('Edit Configuration'),
                 name: () => $gettext('Edit Configuration'),
                 component: () => import('@/views/config/ConfigEdit.vue'),
                 component: () => import('@/views/config/ConfigEdit.vue'),
                 meta: {
                 meta: {
                     hiddenInSidebar: true
                     hiddenInSidebar: true
-                },
+                }
+            },
+            {
+                path: 'cert',
+                name: () => $gettext('Certification'),
+                component: () => import('@/views/cert/Cert.vue'),
+                meta: {
+                    icon: SafetyCertificateOutlined
+                }
             },
             },
             {
             {
                 path: 'terminal',
                 path: 'terminal',
@@ -97,20 +107,28 @@ export const routes = [
                 children: [{
                 children: [{
                     path: 'access',
                     path: 'access',
                     name: () => $gettext('Access Logs'),
                     name: () => $gettext('Access Logs'),
-                    component: () => import('@/views/nginx_log/NginxLog.vue'),
+                    component: () => import('@/views/nginx_log/NginxLog.vue')
                 }, {
                 }, {
                     path: 'error',
                     path: 'error',
                     name: () => $gettext('Error Logs'),
                     name: () => $gettext('Error Logs'),
-                    component: () => import('@/views/nginx_log/NginxLog.vue'),
+                    component: () => import('@/views/nginx_log/NginxLog.vue')
                 }, {
                 }, {
                     path: 'site',
                     path: 'site',
                     name: () => $gettext('Site Logs'),
                     name: () => $gettext('Site Logs'),
                     component: () => import('@/views/nginx_log/NginxLog.vue'),
                     component: () => import('@/views/nginx_log/NginxLog.vue'),
                     meta: {
                     meta: {
                         hiddenInSidebar: true
                         hiddenInSidebar: true
-                    },
+                    }
                 }]
                 }]
             },
             },
+            {
+                path: 'preference',
+                name: () => $gettext('Preference'),
+                component: () => import('@/views/preference/Preference.vue'),
+                meta: {
+                    icon: SettingOutlined
+                }
+            },
             {
             {
                 path: 'about',
                 path: 'about',
                 name: () => $gettext('About'),
                 name: () => $gettext('About'),
@@ -118,7 +136,7 @@ export const routes = [
                 meta: {
                 meta: {
                     icon: InfoCircleOutlined
                     icon: InfoCircleOutlined
                 }
                 }
-            },
+            }
         ]
         ]
     },
     },
     {
     {
@@ -144,7 +162,7 @@ export const routes = [
 const router = createRouter({
 const router = createRouter({
     history: createWebHistory(),
     history: createWebHistory(),
     // @ts-ignore
     // @ts-ignore
-    routes: routes,
+    routes: routes
 })
 })
 
 
 router.beforeEach((to, from, next) => {
 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 StdTable from '@/components/StdDataDisplay/StdTable.vue'
 import gettext from '@/gettext'
 import gettext from '@/gettext'
 import config from '@/api/config'
 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 {$gettext} = gettext
 
 
 const api = config
 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>
 </script>
+
 <template>
 <template>
     <a-card :title="$gettext('Configurations')">
     <a-card :title="$gettext('Configurations')">
         <std-table
         <std-table
+            :key="update"
+            ref="table"
             :api="api"
             :api="api"
-            :columns="columns"
+            :columns="configColumns"
             :deletable="false"
             :deletable="false"
             :disable_search="true"
             :disable_search="true"
             row-key="name"
             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>
     </a-card>
 </template>
 </template>
 
 

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

@@ -2,15 +2,22 @@
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
 import FooterToolBar from '@/components/FooterToolbar/FooterToolBar.vue'
 import gettext from '@/gettext'
 import gettext from '@/gettext'
 import {useRoute} from 'vue-router'
 import {useRoute} from 'vue-router'
-import {ref} from 'vue'
+import {computed, ref} from 'vue'
 import config from '@/api/config'
 import config from '@/api/config'
 import {message} from 'ant-design-vue'
 import {message} from 'ant-design-vue'
 import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
 import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
+import ngx from '@/api/ngx'
 
 
 const {$gettext, interpolate} = gettext
 const {$gettext, interpolate} = gettext
 const route = useRoute()
 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('')
 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>
 </script>
 
 
 
 
@@ -46,7 +61,10 @@ function save() {
         <footer-tool-bar>
         <footer-tool-bar>
             <a-space>
             <a-space>
                 <a-button @click="$router.go(-1)">
                 <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>
                 <a-button type="primary" @click="save">
                 <a-button type="primary" @click="save">
                     <translate>Save</translate>
                     <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 {computed, reactive, ref} from 'vue'
 import {message} from 'ant-design-vue'
 import {message} from 'ant-design-vue'
 import {useRouter} from 'vue-router'
 import {useRouter} from 'vue-router'
+import template from '@/api/template'
 
 
 const {$gettext, interpolate} = useGettext()
 const {$gettext, interpolate} = useGettext()
 
 
@@ -39,7 +40,7 @@ function init() {
 
 
 function save() {
 function save() {
     ngx.build_config(ngx_config).then(r => {
     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'))
             message.success($gettext('Saved successfully'))
 
 
             domain.enable(config.name).then(() => {
             domain.enable(config.name).then(() => {
@@ -89,7 +90,6 @@ const has_server_name = computed(() => {
                 <a-step :title="$gettext('Configure SSL')"/>
                 <a-step :title="$gettext('Configure SSL')"/>
                 <a-step :title="$gettext('Finished')"/>
                 <a-step :title="$gettext('Finished')"/>
             </a-steps>
             </a-steps>
-
             <template v-if="current_step===0">
             <template v-if="current_step===0">
                 <a-form layout="vertical">
                 <a-form layout="vertical">
                     <a-form-item :label="$gettext('Configuration Name')">
                     <a-form-item :label="$gettext('Configuration Name')">
@@ -97,7 +97,6 @@ const has_server_name = computed(() => {
                     </a-form-item>
                     </a-form-item>
                 </a-form>
                 </a-form>
 
 
-
                 <directive-editor :ngx_directives="ngx_config.servers[0].directives"/>
                 <directive-editor :ngx_directives="ngx_config.servers[0].directives"/>
                 <br/>
                 <br/>
                 <location-editor :locations="ngx_config.servers[0].locations"/>
                 <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 NgxConfigEditor from '@/views/domain/ngx_conf/NgxConfigEditor'
 import {useGettext} from 'vue3-gettext'
 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 domain from '@/api/domain'
 import ngx from '@/api/ngx'
 import ngx from '@/api/ngx'
 import {message} from 'ant-design-vue'
 import {message} from 'ant-design-vue'
@@ -14,8 +14,13 @@ import {message} from 'ant-design-vue'
 const {$gettext, interpolate} = useGettext()
 const {$gettext, interpolate} = useGettext()
 
 
 const route = useRoute()
 const route = useRoute()
+const router = useRouter()
 
 
 const name = ref(route.params.name.toString())
 const name = ref(route.params.name.toString())
+watch(route, () => {
+    name.value = route.params?.name?.toString() ?? ''
+})
+
 const update = ref(0)
 const update = ref(0)
 
 
 const ngx_config = reactive({
 const ngx_config = reactive({
@@ -32,6 +37,7 @@ const configText = ref('')
 const ok = ref(false)
 const ok = ref(false)
 const advance_mode = ref(false)
 const advance_mode = ref(false)
 const saving = ref(false)
 const saving = ref(false)
+const filename = ref('')
 
 
 init()
 init()
 
 
@@ -40,7 +46,7 @@ function handle_response(r: any) {
     Object.keys(cert_info_map).forEach(v => {
     Object.keys(cert_info_map).forEach(v => {
         delete cert_info_map[v]
         delete cert_info_map[v]
     })
     })
-
+    filename.value = r.name
     configText.value = r.config
     configText.value = r.config
     enabled.value = r.enabled
     enabled.value = r.enabled
     auto_cert.value = r.auto_cert
     auto_cert.value = r.auto_cert
@@ -85,9 +91,9 @@ const save = async () => {
         await build_config()
         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)
         handle_response(r)
-
+        router.push('/domain/' + filename.value)
         message.success($gettext('Saved successfully'))
         message.success($gettext('Saved successfully'))
 
 
     }).catch((e: any) => {
     }).catch((e: any) => {
@@ -159,6 +165,9 @@ function on_change_enabled(checked: boolean) {
                     <a-form-item :label="$gettext('Enabled')">
                     <a-form-item :label="$gettext('Enabled')">
                         <a-switch v-model:checked="enabled" @change="on_change_enabled"/>
                         <a-switch v-model:checked="enabled" @change="on_change_enabled"/>
                     </a-form-item>
                     </a-form-item>
+                    <a-form-item :label="$gettext('Name')">
+                        <a-input v-model:value="filename"/>
+                    </a-form-item>
                     <ngx-config-editor
                     <ngx-config-editor
                         ref="ngx_config_editor"
                         ref="ngx_config_editor"
                         :ngx_config="ngx_config"
                         :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(<Badge status="success"/>)
             template.push($gettext('Enabled'))
             template.push($gettext('Enabled'))
         } else {
         } else {
-            template.push(<Badge status="error"/>)
+            template.push(<Badge status="warning"/>)
             template.push($gettext('Disabled'))
             template.push($gettext('Disabled'))
         }
         }
         return h('div', template)
         return h('div', template)
@@ -110,7 +110,7 @@ function destroy(site_name: any) {
                     <a-popconfirm
                     <a-popconfirm
                         :cancelText="$gettext('No')"
                         :cancelText="$gettext('No')"
                         :okText="$gettext('OK')"
                         :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'])">
                         @confirm="destroy(record['name'])">
                         <a v-translate>Delete</a>
                         <a v-translate>Delete</a>
                     </a-popconfirm>
                     </a-popconfirm>

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

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

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

@@ -1,17 +1,12 @@
 <script setup lang="ts">
 <script setup lang="ts">
 import {CloseCircleOutlined, CheckCircleOutlined} from '@ant-design/icons-vue'
 import {CloseCircleOutlined, CheckCircleOutlined} from '@ant-design/icons-vue'
 import dayjs from 'dayjs'
 import dayjs from 'dayjs'
-import {reactive, ref} from 'vue'
-import domain from '@/api/domain'
 
 
 const props = defineProps(['cert'])
 const props = defineProps(['cert'])
-
-const cert = props.cert
 </script>
 </script>
 
 
 <template>
 <template>
     <div class="cert-info" v-if="cert">
     <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="{issuer: cert.issuer_name}">Intermediate Certification Authorities: %{issuer}</p>
         <p v-translate="{name: cert.subject_name}">Subject Name: %{name}</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()}">
         <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 {message} from 'ant-design-vue'
 import domain from '@/api/domain'
 import domain from '@/api/domain'
 import websocket from '@/lib/websocket'
 import websocket from '@/lib/websocket'
+import Template from '@/views/template/Template.vue'
 
 
 const {$gettext, interpolate} = useGettext()
 const {$gettext, interpolate} = useGettext()
 
 
@@ -31,12 +32,6 @@ function job() {
         return
         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]
     const server_name = props.directivesMap['server_name'][0]
 
 
     if (!props.directivesMap['ssl_certificate']) {
     if (!props.directivesMap['ssl_certificate']) {
@@ -62,8 +57,6 @@ function job() {
 function callback(ssl_certificate: string, ssl_certificate_key: string) {
 function callback(ssl_certificate: string, ssl_certificate_key: string) {
     props.directivesMap['ssl_certificate'][0]['params'] = ssl_certificate
     props.directivesMap['ssl_certificate'][0]['params'] = ssl_certificate
     props.directivesMap['ssl_certificate_key'][0]['params'] = ssl_certificate_key
     props.directivesMap['ssl_certificate_key'][0]['params'] = ssl_certificate_key
-
-    emit('callback')
 }
 }
 
 
 function change_auto_cert(r: boolean) {
 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...'))
     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.onopen = () => {
-        ws.send('go')
+        ws.send(JSON.stringify({
+            server_name: server_name.trim().split(' ')
+        }))
     }
     }
 
 
     ws.onmessage = m => {
     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(() => {
 const no_server_name = computed(() => {
-    return props.directivesMap['server_name'].length === 0
+    return props.directivesMap['server_name']?.length === 0
 })
 })
 
 
 const name = computed(() => {
 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, () => {
 watch(no_server_name, () => {
     emit('update:enabled', false)
     emit('update:enabled', false)
     onchange(false)
     onchange(false)
@@ -166,7 +151,7 @@ watch(no_server_name, () => {
 
 
 const progressStrokeColor = {
 const progressStrokeColor = {
     from: '#108ee9',
     from: '#108ee9',
-    to: '#87d068',
+    to: '#87d068'
 }
 }
 
 
 const progressPercent = ref(0)
 const progressPercent = ref(0)
@@ -180,6 +165,7 @@ const modalClosable = ref(false)
     <a-modal
     <a-modal
         :title="$gettext('Obtaining certificate')"
         :title="$gettext('Obtaining certificate')"
         v-model:visible="modalVisible"
         v-model:visible="modalVisible"
+        :mask-closable="modalClosable"
         :footer="null" :closable="modalClosable" force-render>
         :footer="null" :closable="modalClosable" force-render>
         <a-progress
         <a-progress
             :stroke-color="progressStrokeColor"
             :stroke-color="progressStrokeColor"
@@ -191,16 +177,16 @@ const modalClosable = ref(false)
         </div>
         </div>
 
 
     </a-modal>
     </a-modal>
-    <div>
+    <div class="issue-cert">
         <a-form-item :label="$gettext('Encrypt website with Let\'s Encrypt')">
         <a-form-item :label="$gettext('Encrypt website with Let\'s Encrypt')">
             <a-switch
             <a-switch
                 :loading="issuing_cert"
                 :loading="issuing_cert"
                 v-model:checked="enabled"
                 v-model:checked="enabled"
                 @change="onchange"
                 @change="onchange"
-                :disabled="no_server_name||server_name_more_than_one"
+                :disabled="no_server_name"
             />
             />
             <a-alert
             <a-alert
-                v-if="no_server_name||server_name_more_than_one"
+                v-if="no_server_name"
                 :message="$gettext('Warning')"
                 :message="$gettext('Warning')"
                 type="warning"
                 type="warning"
                 show-icon
                 show-icon
@@ -209,24 +195,25 @@ const modalClosable = ref(false)
                     <span v-if="no_server_name" v-translate>
                     <span v-if="no_server_name" v-translate>
                         server_name parameter is required
                         server_name parameter is required
                     </span>
                     </span>
-                    <span v-if="server_name_more_than_one" v-translate>
-                        server_name parameters more than one
-                    </span>
                 </template>
                 </template>
             </a-alert>
             </a-alert>
         </a-form-item>
         </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>
     </div>
 </template>
 </template>
 
 
@@ -247,6 +234,10 @@ const modalClosable = ref(false)
 </style>
 </style>
 
 
 <style lang="less" scoped>
 <style lang="less" scoped>
+.issue-cert {
+    margin: 15px 0;
+}
+
 .switch-wrapper {
 .switch-wrapper {
     position: relative;
     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 {$gettext} = useGettext()
 
 
-const props = defineProps(['locations'])
+const props = defineProps(['locations', 'readonly'])
 
 
 let location = reactive({
 let location = reactive({
     comments: '',
     comments: '',
@@ -52,7 +52,7 @@ function remove(index: number) {
                     <HolderOutlined/>
                     <HolderOutlined/>
                     {{ $gettext('Location') }}
                     {{ $gettext('Location') }}
                 </template>
                 </template>
-                <template #extra>
+                <template #extra v-if="!readonly">
                     <a-popconfirm @confirm="remove(index)"
                     <a-popconfirm @confirm="remove(index)"
                                   :title="$gettext('Are you sure you want to remove this location?')"
                                   :title="$gettext('Are you sure you want to remove this location?')"
                                   :ok-text="$gettext('Yes')"
                                   :ok-text="$gettext('Yes')"
@@ -94,7 +94,7 @@ function remove(index: number) {
         </a-form>
         </a-form>
     </a-modal>
     </a-modal>
 
 
-    <div>
+    <div v-if="!readonly">
         <a-button block @click="add">{{ $gettext('Add Location') }}</a-button>
         <a-button block @click="add">{{ $gettext('Add Location') }}</a-button>
     </div>
     </div>
 </template>
 </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 {useGettext} from 'vue3-gettext'
 import Cert from '@/views/domain/cert/Cert.vue'
 import Cert from '@/views/domain/cert/Cert.vue'
 import LogEntry from '@/views/domain/ngx_conf/LogEntry.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()
 const {$gettext} = useGettext()
 
 
@@ -151,6 +153,9 @@ watch(current_server_index, () => {
             <a-switch @change="change_tls"/>
             <a-switch @change="change_tls"/>
         </a-form-item>
         </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-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">
             <a-tab-pane :tab="'Server '+(k+1)" v-for="(v,k) in props.ngx_config.servers" :key="k">
                 <log-entry
                 <log-entry
@@ -175,9 +180,11 @@ watch(current_server_index, () => {
                         <h3 v-translate>Comments</h3>
                         <h3 v-translate>Comments</h3>
                         <a-textarea v-model:value="v.comments" :bordered="false"/>
                         <a-textarea v-model:value="v.comments" :bordered="false"/>
                     </template>
                     </template>
-
                     <directive-editor :ngx_directives="v.directives"/>
                     <directive-editor :ngx_directives="v.directives"/>
                     <br/>
                     <br/>
+                    <config-template :ngx_config="ngx_config"
+                                     :current_server_index="current_server_index"/>
+                    <br/>
                     <location-editor :locations="v.locations"/>
                     <location-editor :locations="v.locations"/>
                 </div>
                 </div>
 
 

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

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

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

@@ -1,17 +1,13 @@
 <script setup lang="ts">
 <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 DirectiveAdd from '@/views/domain/ngx_conf/directive/DirectiveAdd'
 import {useGettext} from 'vue3-gettext'
 import {useGettext} from 'vue3-gettext'
 import {reactive, ref} from 'vue'
 import {reactive, ref} from 'vue'
-import {DeleteOutlined, HolderOutlined} from '@ant-design/icons-vue'
 import draggable from 'vuedraggable'
 import draggable from 'vuedraggable'
+import DirectiveEditorItem from '@/views/domain/ngx_conf/directive/DirectiveEditorItem.vue'
 
 
 const {$gettext} = useGettext()
 const {$gettext} = useGettext()
 
 
-const props = defineProps<{
-    ngx_directives: any[]
-}>()
+const props = defineProps(['ngx_directives', 'readonly'])
 
 
 const adding = ref(false)
 const adding = ref(false)
 
 
@@ -19,20 +15,6 @@ let directive = reactive({})
 
 
 const current_idx = ref(-1)
 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) {
 function onSave(idx: number) {
     setTimeout(() => {
     setTimeout(() => {
         current_idx.value = idx + 1
         current_idx.value = idx + 1
@@ -48,75 +30,21 @@ function onSave(idx: number) {
         item-key="name"
         item-key="name"
         class="list-group"
         class="list-group"
         ghost-class="ghost"
         ghost-class="ghost"
-        handle=".ant-input-group-addon"
+        handle=".anticon-holder"
     >
     >
         <template #item="{ element: directive, index }">
         <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>
         </template>
     </draggable>
     </draggable>
 
 
-    <directive-add :ngx_directives="props.ngx_directives"/>
+    <directive-add v-if="!readonly" :ngx_directives="ngx_directives"/>
 </template>
 </template>
 
 
 <style lang="less" scoped>
 <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>
 </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, {
     nginx_log.page(0, {
         conf_name: (route.query.conf_name as string),
         conf_name: (route.query.conf_name as string),
         type: logType(),
         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 => {
     }).then(r => {
         page.value = r.page - 1
         page.value = r.page - 1
         addLog(r.content)
         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"
   resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.1.tgz#75b4c27948c81e88ccd3a8902047bcd797f38d32"
   integrity sha512-ej5oVy6lykXsvieQtqZxCOaLT+xD4+QNarq78cIYISHmZXshCvROLudpQN3lfL8G0NL7plMSSK+zlyvCaIJ4Iw==
   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":
 "@esbuild/linux-loong64@0.14.53":
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.53.tgz#251b4cd6760fadb4d68a05815e6dc5e432d69cd6"
   resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.14.53.tgz#251b4cd6760fadb4d68a05815e6dc5e432d69cd6"
   integrity sha512-W2dAL6Bnyn4xa/QRSU3ilIK4EzD5wgYXKXJiS1HDF5vU3675qc2bvFyLwbUcdmssDveyndy7FbitrCoiV/eMLg==
   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":
 "@jridgewell/gen-mapping@^0.1.0":
   version "0.1.1"
   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"
   resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.53.tgz#259bc3ef1399a3cad8f4f67c40ee20779c4de675"
   integrity sha512-fIL93sOTnEU+NrTAVMIKiAw0YH22HWCAgg4N4Z6zov2t0kY9RAJ50zY9ZMCQ+RT6bnOfDt8gCTnt/RaSNA2yRA==
   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:
 esbuild-android-arm64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.53.tgz#2158253d4e8f9fdd2a081bbb4f73b8806178841e"
   resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.53.tgz#2158253d4e8f9fdd2a081bbb4f73b8806178841e"
   integrity sha512-PC7KaF1v0h/nWpvlU1UMN7dzB54cBH8qSsm7S9mkwFA1BXpaEOufCg8hdoEI1jep0KeO/rjZVWrsH8+q28T77A==
   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:
 esbuild-darwin-64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.53.tgz#b4681831fd8f8d06feb5048acbe90d742074cc2a"
   resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.53.tgz#b4681831fd8f8d06feb5048acbe90d742074cc2a"
   integrity sha512-gE7P5wlnkX4d4PKvLBUgmhZXvL7lzGRLri17/+CmmCzfncIgq8lOBvxGMiQ4xazplhxq+72TEohyFMZLFxuWvg==
   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:
 esbuild-darwin-arm64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.53.tgz#d267d957852d121b261b3f76ead86e5b5463acc9"
   resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.53.tgz#d267d957852d121b261b3f76ead86e5b5463acc9"
   integrity sha512-otJwDU3hnI15Q98PX4MJbknSZ/WSR1I45il7gcxcECXzfN4Mrpft5hBDHXNRnCh+5858uPXBXA1Vaz2jVWLaIA==
   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:
 esbuild-freebsd-64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.53.tgz#aca2af6d72b537fe66a38eb8f374fb66d4c98ca0"
   resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.53.tgz#aca2af6d72b537fe66a38eb8f374fb66d4c98ca0"
   integrity sha512-WkdJa8iyrGHyKiPF4lk0MiOF87Q2SkE+i+8D4Cazq3/iqmGPJ6u49je300MFi5I2eUsQCkaOWhpCVQMTKGww2w==
   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:
 esbuild-freebsd-arm64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.53.tgz#76282e19312d914c34343c8a7da6cc5f051580b9"
   resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.53.tgz#76282e19312d914c34343c8a7da6cc5f051580b9"
   integrity sha512-9T7WwCuV30NAx0SyQpw8edbKvbKELnnm1FHg7gbSYaatH+c8WJW10g/OdM7JYnv7qkimw2ZTtSA+NokOLd2ydQ==
   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:
 esbuild-linux-32@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.53.tgz#1045d34cf7c5faaf2af3b29cc1573b06580c37e5"
   resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.53.tgz#1045d34cf7c5faaf2af3b29cc1573b06580c37e5"
   integrity sha512-VGanLBg5en2LfGDgLEUxQko2lqsOS7MTEWUi8x91YmsHNyzJVT/WApbFFx3MQGhkf+XdimVhpyo5/G0PBY91zg==
   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:
 esbuild-linux-64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz#ab3f2ee2ebb5a6930c72d9539cb34b428808cbe4"
   resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.53.tgz#ab3f2ee2ebb5a6930c72d9539cb34b428808cbe4"
   integrity sha512-pP/FA55j/fzAV7N9DF31meAyjOH6Bjuo3aSKPh26+RW85ZEtbJv9nhoxmGTd9FOqjx59Tc1ZbrJabuiXlMwuZQ==
   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:
 esbuild-linux-arm64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.53.tgz#1f5530412f6690949e78297122350488d3266cfe"
   resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.53.tgz#1f5530412f6690949e78297122350488d3266cfe"
   integrity sha512-GDmWITT+PMsjCA6/lByYk7NyFssW4Q6in32iPkpjZ/ytSyH+xeEx8q7HG3AhWH6heemEYEWpTll/eui3jwlSnw==
   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:
 esbuild-linux-arm@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz#a44ec9b5b42007ab6c0d65a224ccc6bbd97c54cf"
   resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.53.tgz#a44ec9b5b42007ab6c0d65a224ccc6bbd97c54cf"
   integrity sha512-/u81NGAVZMopbmzd21Nu/wvnKQK3pT4CrvQ8BTje1STXcQAGnfyKgQlj3m0j2BzYbvQxSy+TMck4TNV2onvoPA==
   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:
 esbuild-linux-mips64le@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.53.tgz#a4d0b6b17cfdeea4e41b0b085a5f73d99311be9f"
   resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.53.tgz#a4d0b6b17cfdeea4e41b0b085a5f73d99311be9f"
   integrity sha512-d6/XHIQW714gSSp6tOOX2UscedVobELvQlPMkInhx1NPz4ThZI9uNLQ4qQJHGBGKGfu+rtJsxM4NVHLhnNRdWQ==
   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:
 esbuild-linux-ppc64le@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.53.tgz#8c331822c85465434e086e3e6065863770c38139"
   resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.53.tgz#8c331822c85465434e086e3e6065863770c38139"
   integrity sha512-ndnJmniKPCB52m+r6BtHHLAOXw+xBCWIxNnedbIpuREOcbSU/AlyM/2dA3BmUQhsHdb4w3amD5U2s91TJ3MzzA==
   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:
 esbuild-linux-riscv64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.53.tgz#36fd75543401304bea8a2d63bf8ea18aaa508e00"
   resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.53.tgz#36fd75543401304bea8a2d63bf8ea18aaa508e00"
   integrity sha512-yG2sVH+QSix6ct4lIzJj329iJF3MhloLE6/vKMQAAd26UVPVkhMFqFopY+9kCgYsdeWvXdPgmyOuKa48Y7+/EQ==
   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:
 esbuild-linux-s390x@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.53.tgz#1622677ab6824123f48f75d3afc031cd41936129"
   resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.53.tgz#1622677ab6824123f48f75d3afc031cd41936129"
   integrity sha512-OCJlgdkB+XPYndHmw6uZT7jcYgzmx9K+28PVdOa/eLjdoYkeAFvH5hTwX4AXGLZLH09tpl4bVsEtvuyUldaNCg==
   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:
 esbuild-netbsd-64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.53.tgz#e86d0efd0116658be335492ed12e66b26b4baf52"
   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==
   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:
 esbuild-openbsd-64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.53.tgz#9bcbbe6f86304872c6e91f64c8eb73fc29c3588b"
   resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.53.tgz#9bcbbe6f86304872c6e91f64c8eb73fc29c3588b"
   integrity sha512-eKQ30ZWe+WTZmteDYg8S+YjHV5s4iTxeSGhJKJajFfQx9TLZJvsJX0/paqwP51GicOUruFpSUAs2NCc0a4ivQQ==
   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:
 esbuild-sunos-64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.53.tgz#f7a872f7460bfb7b131f7188a95fbce3d1c577e8"
   resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.53.tgz#f7a872f7460bfb7b131f7188a95fbce3d1c577e8"
   integrity sha512-OWLpS7a2FrIRukQqcgQqR1XKn0jSJoOdT+RlhAxUoEQM/IpytS3FXzCJM6xjUYtpO5GMY0EdZJp+ur2pYdm39g==
   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:
 esbuild-windows-32@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.53.tgz#c5e3ca50e2d1439cc2c9fe4defa63bcd474ce709"
   resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.53.tgz#c5e3ca50e2d1439cc2c9fe4defa63bcd474ce709"
   integrity sha512-m14XyWQP5rwGW0tbEfp95U6A0wY0DYPInWBB7D69FAXUpBpBObRoGTKRv36lf2RWOdE4YO3TNvj37zhXjVL5xg==
   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:
 esbuild-windows-64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.53.tgz#ec2ab4a60c5215f092ffe1eab6d01319e88238af"
   resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.53.tgz#ec2ab4a60c5215f092ffe1eab6d01319e88238af"
   integrity sha512-s9skQFF0I7zqnQ2K8S1xdLSfZFsPLuOGmSx57h2btSEswv0N0YodYvqLcJMrNMXh6EynOmWD7rz+0rWWbFpIHQ==
   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:
 esbuild-windows-arm64@0.14.53:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.53.tgz#f71d403806bdf9f4a1f9d097db9aec949bd675c8"
   resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.53.tgz#f71d403806bdf9f4a1f9d097db9aec949bd675c8"
   integrity sha512-E+5Gvb+ZWts+00T9II6wp2L3KG2r3iGxByqd/a1RmLmYWVsSVUjkvIxZuJ3hYTIbhLkH5PRwpldGTKYqVz0nzQ==
   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:
 esbuild@^0.14.47:
   version "0.14.53"
   version "0.14.53"
   resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.53.tgz#20b1007f686e8584f2a01a1bec5a37aac9498ce4"
   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-64 "0.14.53"
     esbuild-windows-arm64 "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:
   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:
 escalade@^3.1.1:
   version "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"
     picocolors "^1.0.0"
     source-map-js "^1.0.2"
     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:
   dependencies:
     nanoid "^3.3.4"
     nanoid "^3.3.4"
     picocolors "^1.0.0"
     picocolors "^1.0.0"
@@ -2642,10 +2642,10 @@ rollup@^2.75.6:
   optionalDependencies:
   optionalDependencies:
     fsevents "~2.3.2"
     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:
   optionalDependencies:
     fsevents "~2.3.2"
     fsevents "~2.3.2"
 
 
@@ -2972,15 +2972,15 @@ vite@^3.0.4:
   optionalDependencies:
   optionalDependencies:
     fsevents "~2.3.2"
     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:
   dependencies:
-    esbuild "^0.15.9"
-    postcss "^8.4.18"
+    esbuild "^0.16.3"
+    postcss "^8.4.20"
     resolve "^1.22.1"
     resolve "^1.22.1"
-    rollup "^2.79.1"
+    rollup "^3.7.0"
   optionalDependencies:
   optionalDependencies:
     fsevents "~2.3.2"
     fsevents "~2.3.2"
 
 

+ 2 - 2
go.mod

@@ -5,7 +5,6 @@ go 1.19
 require (
 require (
 	github.com/creack/pty v1.1.18
 	github.com/creack/pty v1.1.18
 	github.com/dustin/go-humanize v1.0.0
 	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-contrib/static v0.0.1
 	github.com/gin-gonic/gin v1.7.4
 	github.com/gin-gonic/gin v1.7.4
 	github.com/go-acme/lego/v4 v4.4.0
 	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/golang-jwt/jwt v3.2.2+incompatible
 	github.com/google/uuid v1.1.1
 	github.com/google/uuid v1.1.1
 	github.com/gorilla/websocket v1.4.2
 	github.com/gorilla/websocket v1.4.2
+	github.com/hpcloud/tail v1.0.0
 	github.com/pkg/errors v0.9.1
 	github.com/pkg/errors v0.9.1
 	github.com/shirou/gopsutil/v3 v3.21.7
 	github.com/shirou/gopsutil/v3 v3.21.7
 	github.com/spf13/cast v1.3.1
 	github.com/spf13/cast v1.3.1
+	github.com/tufanbarisyildirim/gonginx v0.0.0-20230104065106-9ae864d29eed
 	github.com/unknwon/com v1.0.1
 	github.com/unknwon/com v1.0.1
 	golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
 	golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
 	gopkg.in/ini.v1 v1.62.0
 	gopkg.in/ini.v1 v1.62.0
@@ -32,7 +33,6 @@ require (
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/gin-contrib/sse v0.1.0 // indirect
 	github.com/go-ole/go-ole v1.2.5 // indirect
 	github.com/go-ole/go-ole v1.2.5 // indirect
 	github.com/golang/protobuf v1.3.4 // 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/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.2 // indirect
 	github.com/jinzhu/now v1.1.2 // indirect
 	github.com/json-iterator/go v1.1.9 // 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=
 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=
 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=
 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/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.1.0/go.mod h1:AKyIcETwSUFxIcs/Wnq/C+kwCtlEYGUVd7FPNb2slmg=
 github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
 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-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/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/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/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/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/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/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
 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.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/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/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 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.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.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.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.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-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/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
 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/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/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/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/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 v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
 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/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/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/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.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.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
 go.opencensus.io v0.20.2/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.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.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.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-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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-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-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-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-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 h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU=
 golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 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-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-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-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 h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 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=
 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-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-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-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 h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 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=
 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-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-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.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-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-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-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.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.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 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.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 h1:NAR9A/3SoyiPVHouW/rlpMUZvuQZ6Z6UYGz+2tosSQo=
 gorm.io/gorm v1.21.14/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0=
 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-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-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/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
 package api
 
 
 import (
 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 (
 const (
-	Success = "success"
-	Info    = "info"
-	Error   = "error"
+    Success = "success"
+    Info    = "info"
+    Error   = "error"
 )
 )
 
 
 type IssueCertResponse struct {
 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) {
 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) {
 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) {
 func GetConfigs(c *gin.Context) {
 	orderBy := c.Query("order_by")
 	orderBy := c.Query("order_by")
 	sort := c.DefaultQuery("sort", "desc")
 	sort := c.DefaultQuery("sort", "desc")
+	dir := c.DefaultQuery("dir", "/")
 
 
 	mySort := map[string]string{
 	mySort := map[string]string{
 		"name":   "string",
 		"name":   "string",
 		"modify": "time",
 		"modify": "time",
+		"is_dir": "bool",
 	}
 	}
 
 
-	configFiles, err := os.ReadDir(nginx.GetNginxConfPath("/"))
+	configFiles, err := os.ReadDir(nginx.GetNginxConfPath(dir))
 
 
 	if err != nil {
 	if err != nil {
 		ErrHandler(c, err)
 		ErrHandler(c, err)
@@ -56,14 +58,13 @@ func GetConfigs(c *gin.Context) {
 			if targetInfo.IsDir() {
 			if targetInfo.IsDir() {
 				continue
 				continue
 			}
 			}
-		default:
-			continue
 		}
 		}
 
 
 		configs = append(configs, gin.H{
 		configs = append(configs, gin.H{
 			"name":   file.Name(),
 			"name":   file.Name(),
 			"size":   fileInfo.Size(),
 			"size":   fileInfo.Size(),
 			"modify": fileInfo.ModTime(),
 			"modify": fileInfo.ModTime(),
+			"is_dir": file.IsDir(),
 		})
 		})
 	}
 	}
 
 
@@ -109,7 +110,6 @@ func AddConfig(c *gin.Context) {
 
 
 	path := filepath.Join(nginx.GetNginxConfPath("/"), name)
 	path := filepath.Join(nginx.GetNginxConfPath("/"), name)
 
 
-	log.Println(path)
 	if _, err = os.Stat(path); err == nil {
 	if _, err = os.Stat(path); err == nil {
 		c.JSON(http.StatusNotAcceptable, gin.H{
 		c.JSON(http.StatusNotAcceptable, gin.H{
 			"message": "config exist",
 			"message": "config exist",

+ 82 - 11
server/api/domain.go

@@ -77,7 +77,15 @@ type CertificateInfo struct {
 }
 }
 
 
 func GetDomain(c *gin.Context) {
 func GetDomain(c *gin.Context) {
+	rewriteName, ok := c.Get("rewriteConfigFileName")
+
 	name := c.Param("name")
 	name := c.Param("name")
+
+	// for modify filename
+	if ok {
+		name = rewriteName.(string)
+	}
+
 	path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
 	path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
 
 
 	enabled := true
 	enabled := true
@@ -93,8 +101,15 @@ func GetDomain(c *gin.Context) {
 	}
 	}
 
 
 	certInfoMap := make(map[int]CertificateInfo)
 	certInfoMap := make(map[int]CertificateInfo)
+	var serverName string
 	for serverIdx, server := range config.Servers {
 	for serverIdx, server := range config.Servers {
 		for _, directive := range server.Directives {
 		for _, directive := range server.Directives {
+
+			if directive.Directive == "server_name" {
+				serverName = strings.ReplaceAll(directive.Params, " ", "_")
+				continue
+			}
+
 			if directive.Directive == "ssl_certificate" {
 			if directive.Directive == "ssl_certificate" {
 
 
 				pubKey, err := cert.GetCertInfo(directive.Params)
 				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{
 	c.JSON(http.StatusOK, gin.H{
 		"enabled":   enabled,
 		"enabled":   enabled,
 		"name":      name,
 		"name":      name,
-		"config":    config.BuildConfig(),
+		"config":    config.FmtCode(),
 		"tokenized": config,
 		"tokenized": config,
-		"auto_cert": err == nil,
+		"auto_cert": certModel.AutoCert == model.AutoCertEnabled,
 		"cert_info": certInfoMap,
 		"cert_info": certInfoMap,
 	})
 	})
 
 
 }
 }
 
 
 func EditDomain(c *gin.Context) {
 func EditDomain(c *gin.Context) {
-	var err error
 	name := c.Param("name")
 	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)
 	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 {
 	if err != nil {
 		ErrHandler(c, err)
 		ErrHandler(c, err)
 		return
 		return
 	}
 	}
-
 	enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
 	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 {
 	if _, err = os.Stat(enabledConfigFilePath); err == nil {
 		// Test nginx configuration
 		// Test nginx configuration
 		err = nginx.TestNginxConf()
 		err = nginx.TestNginxConf()
@@ -287,20 +342,36 @@ func DeleteDomain(c *gin.Context) {
 
 
 func AddDomainToAutoCert(c *gin.Context) {
 func AddDomainToAutoCert(c *gin.Context) {
 	domain := c.Param("domain")
 	domain := c.Param("domain")
-
+	domain = strings.ReplaceAll(domain, " ", "_")
 	certModel, err := model.FirstOrCreateCert(domain)
 	certModel, err := model.FirstOrCreateCert(domain)
+
 	if err != nil {
 	if err != nil {
 		ErrHandler(c, err)
 		ErrHandler(c, err)
 		return
 		return
 	}
 	}
+
+	err = certModel.Updates(&model.Cert{
+		AutoCert: model.AutoCertEnabled,
+	})
+
+	if err != nil {
+		ErrHandler(c, err)
+		return
+	}
+
 	c.JSON(http.StatusOK, certModel)
 	c.JSON(http.StatusOK, certModel)
 }
 }
 
 
 func RemoveDomainFromAutoCert(c *gin.Context) {
 func RemoveDomainFromAutoCert(c *gin.Context) {
+	domain := c.Param("domain")
+	domain = strings.ReplaceAll(domain, " ", "_")
 	certModel := model.Cert{
 	certModel := model.Cert{
-		Domain: c.Param("domain"),
+		Domain: domain,
 	}
 	}
-	err := certModel.Remove()
+
+	err := certModel.Updates(&model.Cert{
+		AutoCert: model.AutoCertDisabled,
+	})
 
 
 	if err != nil {
 	if err != nil {
 		ErrHandler(c, err)
 		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]
 		directive := config.Servers[control.ServerIdx].Directives[control.DirectiveIdx]
-
 		switch directive.Directive {
 		switch directive.Directive {
 		case "access_log", "error_log":
 		case "access_log", "error_log":
 			// ok
 			// ok

+ 31 - 26
server/api/ngx.go

@@ -1,42 +1,47 @@
 package api
 package api
 
 
 import (
 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) {
 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) {
 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 (
 import (
 	"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
 	"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+	"github.com/0xJacky/Nginx-UI/server/service"
 	"github.com/0xJacky/Nginx-UI/server/settings"
 	"github.com/0xJacky/Nginx-UI/server/settings"
 	"github.com/gin-gonic/gin"
 	"github.com/gin-gonic/gin"
 	"net/http"
 	"net/http"
@@ -57,3 +58,45 @@ proxy_pass http://127.0.0.1:{{ HTTP01PORT }};
 		"tokenized": ngxConfig,
 		"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"
 	"path/filepath"
 )
 )
 
 
+const (
+	AutoCertEnabled  = 1
+	AutoCertDisabled = -1
+)
+
 type Cert struct {
 type Cert struct {
 	Model
 	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) {
 func FirstCert(domain string) (c Cert, err error) {
@@ -27,7 +35,7 @@ func FirstOrCreateCert(domain string) (c Cert, err error) {
 
 
 func GetAutoCertList() (c []Cert) {
 func GetAutoCertList() (c []Cert) {
 	var t []Cert
 	var t []Cert
-	db.Find(&t)
+	db.Where("auto_cert", AutoCertEnabled).Find(&t)
 
 
 	// check if this domain is enabled
 	// check if this domain is enabled
 	enabledConfig, err := os.ReadDir(filepath.Join(nginx.GetNginxConfPath("sites-enabled")))
 	enabledConfig, err := os.ReadDir(filepath.Join(nginx.GetNginxConfPath("sites-enabled")))
@@ -50,6 +58,24 @@ func GetAutoCertList() (c []Cert) {
 	return
 	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 {
 func (c *Cert) Updates(n *Cert) error {
 	return db.Model(c).Updates(n).Error
 	return db.Model(c).Updates(n).Error
 }
 }

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

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

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

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

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

@@ -1,88 +1,90 @@
 package nginx
 package nginx
 
 
 import (
 import (
-    "bufio"
-    "fmt"
-    "strings"
+	"bufio"
+	"fmt"
+	"github.com/tufanbarisyildirim/gonginx"
+	"github.com/tufanbarisyildirim/gonginx/parser"
+	"strings"
 )
 )
 
 
 func buildComments(orig string, indent int) (content string) {
 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) {
 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
 package nginx
 
 
 import (
 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
 package nginx
 
 
 import (
 import (
-	"bufio"
-	"github.com/emirpasic/gods/stacks/linkedliststack"
 	"github.com/pkg/errors"
 	"github.com/pkg/errors"
-	"os"
+	"github.com/tufanbarisyildirim/gonginx"
+	"github.com/tufanbarisyildirim/gonginx/parser"
 	"strings"
 	"strings"
-	"unicode"
 )
 )
 
 
 const (
 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
 		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
 		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
 				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:
 		case Server:
-			c.parseServer(paramsScanner)
+			server := NewNgxServer()
+			server.Comments = comments
+			server.parseServer(v)
+			ngxConfig.Servers = append(ngxConfig.Servers, server)
 		case Upstream:
 		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:
 		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 {
 	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
 package nginx
 
 
 import (
 import (
-	"github.com/emirpasic/gods/queues/linkedlistqueue"
+	"github.com/tufanbarisyildirim/gonginx"
 	"strings"
 	"strings"
 )
 )
 
 
-type CommentQueue struct {
-	*linkedlistqueue.Queue
-}
-
 type NgxConfig struct {
 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 {
 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 {
 type NgxUpstream struct {
@@ -42,18 +37,6 @@ type NgxLocation struct {
 	Comments string `json:"comments"`
 	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 {
 func (d *NgxDirective) Orig() string {
 	return d.Directive + " " + d.Params
 	return d.Directive + " " + d.Params
 }
 }
@@ -65,16 +48,14 @@ func (d *NgxDirective) TrimParams() {
 
 
 func NewNgxServer() *NgxServer {
 func NewNgxServer() *NgxServer {
 	return &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 {
 func NewNgxConfig(filename string) *NgxConfig {
 	return &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 (
 import (
 	"bufio"
 	"bufio"
 	"github.com/0xJacky/Nginx-UI/server/api"
 	"github.com/0xJacky/Nginx-UI/server/api"
-	"github.com/0xJacky/Nginx-UI/server/settings"
 	"github.com/gin-contrib/static"
 	"github.com/gin-contrib/static"
 	"github.com/gin-gonic/gin"
 	"github.com/gin-gonic/gin"
 	"net/http"
 	"net/http"
@@ -34,13 +33,6 @@ func InitRouter() *gin.Engine {
 
 
 	root := r.Group("/api")
 	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.GET("install", api.InstallLockCheck)
 		root.POST("install", api.InstallNginxUI)
 		root.POST("install", api.InstallNginxUI)
 
 
@@ -68,27 +60,37 @@ func InitRouter() *gin.Engine {
 			g.POST("ngx/build_config", api.BuildNginxConfig)
 			g.POST("ngx/build_config", api.BuildNginxConfig)
 			// Tokenized nginx configuration to NgxConf
 			// Tokenized nginx configuration to NgxConf
 			g.POST("ngx/tokenize_config", api.TokenizeNginxConfig)
 			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/enable", api.EnableDomain)
 			g.POST("domain/:name/disable", api.DisableDomain)
 			g.POST("domain/:name/disable", api.DisableDomain)
 			g.DELETE("domain/:name", api.DeleteDomain)
 			g.DELETE("domain/:name", api.DeleteDomain)
 
 
 			g.GET("configs", api.GetConfigs)
 			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", api.AddConfig)
-			g.POST("config/:name", api.EditConfig)
+			g.POST("config/*name", api.EditConfig)
 
 
 			//g.GET("backups", api.GetFileBackupList)
 			//g.GET("backups", api.GetFileBackupList)
 			//g.GET("backup/:id", api.GetFileBackup)
 			//g.GET("backup/:id", api.GetFileBackup)
 
 
 			g.GET("template", api.GetTemplate)
 			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
 			// 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
 			// Delete domain from auto-renew cert list
-			g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
+			g.DELETE("auto_cert/:domain", api.RemoveDomainFromAutoCert)
 
 
 			// pty
 			// pty
 			g.GET("pty", api.Pty)
 			g.GET("pty", api.Pty)
@@ -96,6 +98,10 @@ func InitRouter() *gin.Engine {
 			// Nginx log
 			// Nginx log
 			g.GET("nginx_log", api.NginxLog)
 			g.GET("nginx_log", api.NginxLog)
 			g.POST("nginx_log", api.GetNginxLogPage)
 			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 {
 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 {
 type NginxLog struct {
-	AccessLogPath string
-	ErrorLogPath  string
+	AccessLogPath string `json:"access_log_path"`
+	ErrorLogPath  string `json:"error_log_path"`
 }
 }
 
 
 var ServerSettings = &Server{
 var ServerSettings = &Server{
@@ -41,6 +41,7 @@ var ServerSettings = &Server{
 	StartCmd:          "login",
 	StartCmd:          "login",
 	Demo:              false,
 	Demo:              false,
 	PageSize:          10,
 	PageSize:          10,
+	CADir:             "",
 }
 }
 
 
 var NginxLogSettings = &NginxLog{
 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

Some files were not shown because too many files changed in this diff