0xJacky 3 年之前
父節點
當前提交
460480c64a

+ 1 - 0
.gitignore

@@ -7,3 +7,4 @@ app.ini
 dist
 dist
 *.exe
 *.exe
 *.po~
 *.po~
+nginx-ui-server

+ 14 - 0
Dockerfile

@@ -0,0 +1,14 @@
+# CGO_ENABLED=1 GOOS=linux CC=x86_64-unknown-linux-gnu-gcc CXX=x86_64-unknown-linux-gnu-g++ GOARCH=amd64 go build -o nginx-ui-server -v main.go
+FROM --platform=linux/amd64 debian:buster
+WORKDIR /app
+COPY ./resources/demo/sources.list /etc/apt/sources.list
+RUN cd /app && apt-get update -y && apt install nginx curl -y
+EXPOSE 80
+COPY ./resources/demo/nginx.conf /etc/nginx/sites-available/default
+COPY ./resources/demo/app.ini /app/app.ini
+COPY ./resources/demo/demo.db /app/database.db
+COPY ./resources/demo/install.sh /app/install.sh
+COPY ./resources/demo/start.sh /app/start.sh
+COPY ./nginx-ui-server /app/nginx-ui
+RUN cd /app && chmod a+x start.sh
+CMD ["./start.sh"]

+ 1 - 1
frontend/package.json

@@ -1,6 +1,6 @@
 {
 {
     "name": "nginx-ui-frontend",
     "name": "nginx-ui-frontend",
-    "version": "1.1.0",
+    "version": "1.2.0",
     "private": true,
     "private": true,
     "scripts": {
     "scripts": {
         "serve": "vue-cli-service serve",
         "serve": "vue-cli-service serve",

+ 3 - 1
frontend/src/api/index.js

@@ -4,6 +4,7 @@ import auth from './auth'
 import user from './user'
 import user from './user'
 import install from './install'
 import install from './install'
 import analytic from './analytic'
 import analytic from './analytic'
+import settings from './settings'
 
 
 export default {
 export default {
     domain,
     domain,
@@ -11,5 +12,6 @@ export default {
     auth,
     auth,
     user,
     user,
     install,
     install,
-    analytic
+    analytic,
+    settings
 }
 }

+ 9 - 0
frontend/src/api/settings.js

@@ -0,0 +1,9 @@
+import http from '@/lib/http'
+
+const settings = {
+    get() {
+        return http.get('/settings')
+    }
+}
+
+export default settings

+ 6 - 11
frontend/src/lib/store/settings.js

@@ -2,27 +2,22 @@ export const settings = {
     namespace: true,
     namespace: true,
     state: {
     state: {
         language: '',
         language: '',
-        translations: {},
+        env: {}
     },
     },
     mutations: {
     mutations: {
         set_language(state, payload) {
         set_language(state, payload) {
             state.language = payload
             state.language = payload
         },
         },
-        update_translations(state, payload) {
-            state.translations = payload
-        }
-    },
-    actions: {
-        set_language({commit}, data) {
-            commit('set_language', data)
-        },
-        update_translations({commit}, data) {
-            commit('update_translations', data)
+        update_env(state, payload) {
+            state.env = {...payload}
         }
         }
     },
     },
     getters: {
     getters: {
         current_language(state) {
         current_language(state) {
             return state.language
             return state.language
+        },
+        env(state) {
+            return state.env
         }
         }
     }
     }
 }
 }

+ 12 - 8
frontend/src/locale/en/LC_MESSAGES/app.po

@@ -44,7 +44,7 @@ msgstr ""
 msgid "Build with"
 msgid "Build with"
 msgstr ""
 msgstr ""
 
 
-#: src/views/config/ConfigEdit.vue:5 src/views/domain/DomainEdit.vue:24
+#: src/views/config/ConfigEdit.vue:5 src/views/domain/DomainEdit.vue:23
 msgid "Cancel"
 msgid "Cancel"
 msgstr ""
 msgstr ""
 
 
@@ -52,11 +52,11 @@ msgstr ""
 msgid "Certificate Auto-renewal"
 msgid "Certificate Auto-renewal"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/CertInfo.vue:11 src/views/domain/CertInfo.vue:2
+#: src/views/domain/CertInfo.vue:12 src/views/domain/CertInfo.vue:2
 msgid "Certificate has expired"
 msgid "Certificate has expired"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/CertInfo.vue:15 src/views/domain/CertInfo.vue:2
+#: src/views/domain/CertInfo.vue:16 src/views/domain/CertInfo.vue:2
 msgid "Certificate is valid"
 msgid "Certificate is valid"
 msgstr ""
 msgstr ""
 
 
@@ -125,7 +125,7 @@ msgstr ""
 msgid "Do you want to change the template to support the TLS?"
 msgid "Do you want to change the template to support the TLS?"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/DomainEdit.vue:38
+#: src/views/domain/DomainEdit.vue:42
 msgid "Edit %{n}"
 msgid "Edit %{n}"
 msgstr ""
 msgstr ""
 
 
@@ -133,7 +133,7 @@ msgstr ""
 msgid "Edit Configuration"
 msgid "Edit Configuration"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/DomainEdit.vue:87
+#: src/views/domain/DomainEdit.vue:95
 msgid "Edit Configuration File"
 msgid "Edit Configuration File"
 msgstr ""
 msgstr ""
 
 
@@ -182,7 +182,7 @@ msgstr ""
 msgid "File Not Found"
 msgid "File Not Found"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/DomainEdit.vue:10 src/views/domain/DomainEdit.vue:4
+#: src/views/domain/DomainEdit.vue:9 src/views/domain/DomainEdit.vue:3
 msgid "Getting Certificate from Let's Encrypt"
 msgid "Getting Certificate from Let's Encrypt"
 msgstr ""
 msgstr ""
 
 
@@ -243,7 +243,7 @@ msgstr ""
 msgid "Logout successful"
 msgid "Logout successful"
 msgstr ""
 msgstr ""
 
 
-#: src/views/domain/DomainEdit.vue:13 src/views/domain/DomainEdit.vue:7
+#: src/views/domain/DomainEdit.vue:12 src/views/domain/DomainEdit.vue:6
 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."
@@ -344,7 +344,7 @@ msgid "Root Directory (root)"
 msgstr ""
 msgstr ""
 
 
 #: src/views/config/ConfigEdit.vue:6 src/views/domain/DomainAdd.vue:6
 #: src/views/config/ConfigEdit.vue:6 src/views/domain/DomainAdd.vue:6
-#: src/views/domain/DomainEdit.vue:25
+#: src/views/domain/DomainEdit.vue:24
 msgid "Save"
 msgid "Save"
 msgstr ""
 msgstr ""
 
 
@@ -418,6 +418,10 @@ msgid ""
 "changed after it has been created."
 "changed after it has been created."
 msgstr ""
 msgstr ""
 
 
+#: src/views/domain/DomainEdit.vue:11 src/views/domain/DomainEdit.vue:5
+msgid "This feature is not available in demo."
+msgstr ""
+
 #: src/views/domain/DomainEdit.vue:134
 #: src/views/domain/DomainEdit.vue:134
 msgid "This operation will lose the custom configuration."
 msgid "This operation will lose the custom configuration."
 msgstr ""
 msgstr ""

二進制
frontend/src/locale/zh_CN/LC_MESSAGES/app.mo


+ 13 - 9
frontend/src/locale/zh_CN/LC_MESSAGES/app.po

@@ -46,7 +46,7 @@ msgstr "成功启用 %{name} 自动续签"
 msgid "Build with"
 msgid "Build with"
 msgstr "构建基于"
 msgstr "构建基于"
 
 
-#: src/views/config/ConfigEdit.vue:5 src/views/domain/DomainEdit.vue:24
+#: src/views/config/ConfigEdit.vue:5 src/views/domain/DomainEdit.vue:23
 msgid "Cancel"
 msgid "Cancel"
 msgstr "取消"
 msgstr "取消"
 
 
@@ -54,11 +54,11 @@ msgstr "取消"
 msgid "Certificate Auto-renewal"
 msgid "Certificate Auto-renewal"
 msgstr "证书自动续签"
 msgstr "证书自动续签"
 
 
-#: src/views/domain/CertInfo.vue:11 src/views/domain/CertInfo.vue:2
+#: src/views/domain/CertInfo.vue:12 src/views/domain/CertInfo.vue:2
 msgid "Certificate has expired"
 msgid "Certificate has expired"
 msgstr "此证书已过期"
 msgstr "此证书已过期"
 
 
-#: src/views/domain/CertInfo.vue:15 src/views/domain/CertInfo.vue:2
+#: src/views/domain/CertInfo.vue:16 src/views/domain/CertInfo.vue:2
 msgid "Certificate is valid"
 msgid "Certificate is valid"
 msgstr "此证书有效"
 msgstr "此证书有效"
 
 
@@ -127,7 +127,7 @@ msgstr "磁盘 IO"
 msgid "Do you want to change the template to support the TLS?"
 msgid "Do you want to change the template to support the TLS?"
 msgstr "你想要改变模板以支持 TLS 吗?"
 msgstr "你想要改变模板以支持 TLS 吗?"
 
 
-#: src/views/domain/DomainEdit.vue:38
+#: src/views/domain/DomainEdit.vue:42
 msgid "Edit %{n}"
 msgid "Edit %{n}"
 msgstr "编辑 %{n}"
 msgstr "编辑 %{n}"
 
 
@@ -135,7 +135,7 @@ msgstr "编辑 %{n}"
 msgid "Edit Configuration"
 msgid "Edit Configuration"
 msgstr "编辑配置"
 msgstr "编辑配置"
 
 
-#: src/views/domain/DomainEdit.vue:87
+#: src/views/domain/DomainEdit.vue:95
 msgid "Edit Configuration File"
 msgid "Edit Configuration File"
 msgstr "编辑配置文件"
 msgstr "编辑配置文件"
 
 
@@ -184,7 +184,7 @@ msgstr "启用失败 %{msg}"
 msgid "File Not Found"
 msgid "File Not Found"
 msgstr "未找到文件"
 msgstr "未找到文件"
 
 
-#: src/views/domain/DomainEdit.vue:10 src/views/domain/DomainEdit.vue:4
+#: src/views/domain/DomainEdit.vue:9 src/views/domain/DomainEdit.vue:3
 msgid "Getting Certificate from Let's Encrypt"
 msgid "Getting Certificate from Let's Encrypt"
 msgstr "从 Let's Encrypt 获取证书"
 msgstr "从 Let's Encrypt 获取证书"
 
 
@@ -245,7 +245,7 @@ msgstr "登录成功"
 msgid "Logout successful"
 msgid "Logout successful"
 msgstr "登出成功"
 msgstr "登出成功"
 
 
-#: src/views/domain/DomainEdit.vue:13 src/views/domain/DomainEdit.vue:7
+#: src/views/domain/DomainEdit.vue:12 src/views/domain/DomainEdit.vue:6
 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."
@@ -275,7 +275,7 @@ msgstr "名称"
 
 
 #: src/views/dashboard/DashBoard.vue:231
 #: src/views/dashboard/DashBoard.vue:231
 msgid "Network"
 msgid "Network"
-msgstr ""
+msgstr "网络"
 
 
 #: src/views/dashboard/DashBoard.vue:165
 #: src/views/dashboard/DashBoard.vue:165
 msgid "Network Total Receive"
 msgid "Network Total Receive"
@@ -348,7 +348,7 @@ msgid "Root Directory (root)"
 msgstr "网站根目录 (root)"
 msgstr "网站根目录 (root)"
 
 
 #: src/views/config/ConfigEdit.vue:6 src/views/domain/DomainAdd.vue:6
 #: src/views/config/ConfigEdit.vue:6 src/views/domain/DomainAdd.vue:6
-#: src/views/domain/DomainEdit.vue:25
+#: src/views/domain/DomainEdit.vue:24
 msgid "Save"
 msgid "Save"
 msgstr "保存"
 msgstr "保存"
 
 
@@ -426,6 +426,10 @@ msgstr ""
 "只有在您的配置文件中有相应字段时,下列的配置才能生效。配置文件名称创建后不"
 "只有在您的配置文件中有相应字段时,下列的配置才能生效。配置文件名称创建后不"
 "可修改。"
 "可修改。"
 
 
+#: src/views/domain/DomainEdit.vue:11 src/views/domain/DomainEdit.vue:5
+msgid "This feature is not available in demo."
+msgstr "该功能在 Demo 中不可用。"
+
 #: src/views/domain/DomainEdit.vue:134
 #: src/views/domain/DomainEdit.vue:134
 msgid "This operation will lose the custom configuration."
 msgid "This operation will lose the custom configuration."
 msgstr "该操作将会丢失自定义配置。"
 msgstr "该操作将会丢失自定义配置。"

+ 12 - 8
frontend/src/locale/zh_TW/LC_MESSAGES/app.po

@@ -47,7 +47,7 @@ msgstr "成功啟用 %{name} 自動續簽"
 msgid "Build with"
 msgid "Build with"
 msgstr "構建基於"
 msgstr "構建基於"
 
 
-#: src/views/config/ConfigEdit.vue:5 src/views/domain/DomainEdit.vue:24
+#: src/views/config/ConfigEdit.vue:5 src/views/domain/DomainEdit.vue:23
 msgid "Cancel"
 msgid "Cancel"
 msgstr "取消"
 msgstr "取消"
 
 
@@ -55,11 +55,11 @@ msgstr "取消"
 msgid "Certificate Auto-renewal"
 msgid "Certificate Auto-renewal"
 msgstr "證書自動續簽"
 msgstr "證書自動續簽"
 
 
-#: src/views/domain/CertInfo.vue:11 src/views/domain/CertInfo.vue:2
+#: src/views/domain/CertInfo.vue:12 src/views/domain/CertInfo.vue:2
 msgid "Certificate has expired"
 msgid "Certificate has expired"
 msgstr "此證書已過期"
 msgstr "此證書已過期"
 
 
-#: src/views/domain/CertInfo.vue:15 src/views/domain/CertInfo.vue:2
+#: src/views/domain/CertInfo.vue:16 src/views/domain/CertInfo.vue:2
 msgid "Certificate is valid"
 msgid "Certificate is valid"
 msgstr "此證書有效"
 msgstr "此證書有效"
 
 
@@ -129,7 +129,7 @@ msgstr ""
 msgid "Do you want to change the template to support the TLS?"
 msgid "Do you want to change the template to support the TLS?"
 msgstr "你想要改變模板以支援 TLS 嗎?"
 msgstr "你想要改變模板以支援 TLS 嗎?"
 
 
-#: src/views/domain/DomainEdit.vue:38
+#: src/views/domain/DomainEdit.vue:42
 msgid "Edit %{n}"
 msgid "Edit %{n}"
 msgstr "編輯 %{n}"
 msgstr "編輯 %{n}"
 
 
@@ -137,7 +137,7 @@ msgstr "編輯 %{n}"
 msgid "Edit Configuration"
 msgid "Edit Configuration"
 msgstr "編輯配置"
 msgstr "編輯配置"
 
 
-#: src/views/domain/DomainEdit.vue:87
+#: src/views/domain/DomainEdit.vue:95
 msgid "Edit Configuration File"
 msgid "Edit Configuration File"
 msgstr "編輯配置檔案"
 msgstr "編輯配置檔案"
 
 
@@ -186,7 +186,7 @@ msgstr "啟用失敗 %{msg}"
 msgid "File Not Found"
 msgid "File Not Found"
 msgstr "未找到檔案"
 msgstr "未找到檔案"
 
 
-#: src/views/domain/DomainEdit.vue:10 src/views/domain/DomainEdit.vue:4
+#: src/views/domain/DomainEdit.vue:9 src/views/domain/DomainEdit.vue:3
 msgid "Getting Certificate from Let's Encrypt"
 msgid "Getting Certificate from Let's Encrypt"
 msgstr "從 Let's Encrypt 獲取證書"
 msgstr "從 Let's Encrypt 獲取證書"
 
 
@@ -247,7 +247,7 @@ msgstr "登入成功"
 msgid "Logout successful"
 msgid "Logout successful"
 msgstr "登出成功"
 msgstr "登出成功"
 
 
-#: src/views/domain/DomainEdit.vue:13 src/views/domain/DomainEdit.vue:7
+#: src/views/domain/DomainEdit.vue:12 src/views/domain/DomainEdit.vue:6
 #, fuzzy
 #, fuzzy
 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 "
@@ -351,7 +351,7 @@ msgid "Root Directory (root)"
 msgstr "網站根目錄 (root)"
 msgstr "網站根目錄 (root)"
 
 
 #: src/views/config/ConfigEdit.vue:6 src/views/domain/DomainAdd.vue:6
 #: src/views/config/ConfigEdit.vue:6 src/views/domain/DomainAdd.vue:6
-#: src/views/domain/DomainEdit.vue:25
+#: src/views/domain/DomainEdit.vue:24
 msgid "Save"
 msgid "Save"
 msgstr "儲存"
 msgstr "儲存"
 
 
@@ -430,6 +430,10 @@ msgstr ""
 "只有在您的配置檔案中有相應欄位時,下列的配置才能生效。配置檔名稱建立後不可修"
 "只有在您的配置檔案中有相應欄位時,下列的配置才能生效。配置檔名稱建立後不可修"
 "改。"
 "改。"
 
 
+#: src/views/domain/DomainEdit.vue:11 src/views/domain/DomainEdit.vue:5
+msgid "This feature is not available in demo."
+msgstr ""
+
 #: src/views/domain/DomainEdit.vue:134
 #: src/views/domain/DomainEdit.vue:134
 msgid "This operation will lose the custom configuration."
 msgid "This operation will lose the custom configuration."
 msgstr "該操作將會丟失自定義配置。"
 msgstr "該操作將會丟失自定義配置。"

+ 4 - 0
frontend/src/main.js

@@ -20,6 +20,10 @@ Vue.config.productionTip = false
 Vue.prototype.$routeConfig = routes
 Vue.prototype.$routeConfig = routes
 Vue.prototype.$api = api
 Vue.prototype.$api = api
 
 
+api.settings.get().then(r => {
+    store.commit('update_env', r)
+})
+
 Vue.use(GetTextPlugin, {
 Vue.use(GetTextPlugin, {
     availableLanguages,
     availableLanguages,
     defaultLanguage: store.getters.current_language,
     defaultLanguage: store.getters.current_language,

文件差異過大導致無法顯示
+ 0 - 0
frontend/src/translations.json


+ 11 - 2
frontend/src/views/domain/DomainEdit.vue

@@ -6,10 +6,16 @@
                 <std-data-entry :data-list="columns" v-model="config"/>
                 <std-data-entry :data-list="columns" v-model="config"/>
                 <template v-if="config.support_ssl">
                 <template v-if="config.support_ssl">
                     <cert-info :domain="name" ref="cert-info" v-if="name"/>
                     <cert-info :domain="name" ref="cert-info" v-if="name"/>
-                    <a-button @click="issue_cert" type="primary" ghost style="margin: 10px 0">
+                    <a-button
+                        @click="issue_cert"
+                        type="primary" ghost
+                        style="margin: 10px 0"
+                        :disabled="is_demo"
+                    >
                         <translate>Getting Certificate from Let's Encrypt</translate>
                         <translate>Getting Certificate from Let's Encrypt</translate>
                     </a-button>
                     </a-button>
-                    <p v-translate>Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.</p>
+                    <p v-if="is_demo" v-translate>This feature is not available in demo.</p>
+                    <p v-else v-translate>Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.</p>
                 </template>
                 </template>
             </a-collapse-panel>
             </a-collapse-panel>
         </a-collapse>
         </a-collapse>
@@ -238,6 +244,9 @@ export default {
                     return [...columns]
                     return [...columns]
                 }
                 }
             }
             }
+        },
+        is_demo() {
+            return this.$store.getters.env.demo===true
         }
         }
     }
     }
 }
 }

+ 1 - 1
frontend/version.json

@@ -1 +1 @@
-{"version":"1.1.0","build_id":23,"total_build":40}
+{"version":"1.2.0","build_id":2,"total_build":42}

+ 4 - 3
go.mod

@@ -3,7 +3,6 @@ module github.com/0xJacky/Nginx-UI
 go 1.17
 go 1.17
 
 
 require (
 require (
-	github.com/0xJacky/pofile v0.0.1
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/dgrijalva/jwt-go v3.2.0+incompatible
 	github.com/dustin/go-humanize v1.0.0
 	github.com/dustin/go-humanize v1.0.0
 	github.com/gin-contrib/static v0.0.1
 	github.com/gin-contrib/static v0.0.1
@@ -16,6 +15,7 @@ require (
 	github.com/gorilla/websocket v1.4.2
 	github.com/gorilla/websocket v1.4.2
 	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/stretchr/testify v1.7.0
 	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
@@ -26,10 +26,10 @@ require (
 require (
 require (
 	github.com/StackExchange/wmi v1.2.1 // indirect
 	github.com/StackExchange/wmi v1.2.1 // indirect
 	github.com/cenkalti/backoff/v4 v4.1.0 // indirect
 	github.com/cenkalti/backoff/v4 v4.1.0 // indirect
+	github.com/davecgh/go-spew v1.1.1 // indirect
 	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/itchyny/timefmt-go v0.1.3 // 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
@@ -39,7 +39,7 @@ require (
 	github.com/miekg/dns v1.1.40 // indirect
 	github.com/miekg/dns v1.1.40 // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.1 // indirect
 	github.com/modern-go/reflect2 v1.0.1 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/tklauser/go-sysconf v0.3.7 // indirect
 	github.com/tklauser/go-sysconf v0.3.7 // indirect
 	github.com/tklauser/numcpus v0.2.3 // indirect
 	github.com/tklauser/numcpus v0.2.3 // indirect
 	github.com/ugorji/go/codec v1.1.7 // indirect
 	github.com/ugorji/go/codec v1.1.7 // indirect
@@ -48,4 +48,5 @@ require (
 	golang.org/x/text v0.3.4 // indirect
 	golang.org/x/text v0.3.4 // indirect
 	gopkg.in/square/go-jose.v2 v2.5.1 // indirect
 	gopkg.in/square/go-jose.v2 v2.5.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
 )
 )

+ 0 - 7
go.sum

@@ -23,10 +23,6 @@ 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/pofile v0.0.0-20220219101524-60ce48e4de23 h1:nqaxj4ZYzLZzhFQeX1ZrXEBz0haUu7PypGFhQbQfDX0=
-github.com/0xJacky/pofile v0.0.0-20220219101524-60ce48e4de23/go.mod h1:gSDWobvodMtvwh7FE/F999AQoCwBoXgzyGffYFX9nKA=
-github.com/0xJacky/pofile v0.0.1 h1:hVRaw6ZHkajSMAuP58WMDTvGF8+OF297jpAchFK/4rQ=
-github.com/0xJacky/pofile v0.0.1/go.mod h1:gSDWobvodMtvwh7FE/F999AQoCwBoXgzyGffYFX9nKA=
 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=
@@ -229,8 +225,6 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
 github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
 github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhKWFeDesPjMj+wCHReeknARU3wqlyN4=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
 github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
 github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
-github.com/itchyny/timefmt-go v0.1.3 h1:7M3LGVDsqcd0VZH2U+x393obrzZisp7C0uEe921iRkU=
-github.com/itchyny/timefmt-go v0.1.3/go.mod h1:0osSSCQSASBJMsIZnhAaF1C2fCBTJZXrnj37mG8/c+A=
 github.com/jarcoal/httpmock v1.0.6/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
 github.com/jarcoal/httpmock v1.0.6/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
 github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
@@ -341,7 +335,6 @@ github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrap
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
 github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
 github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=
 github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw=

二進制
resources/demo/demo.db


+ 78 - 0
resources/demo/install.sh

@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+
+PROXY=""
+RPROXY="https://ghproxy.com/"
+
+MACHINE="amd64"
+
+# Font color
+FontBlack="\033[30m";
+FontRed="\033[31m";
+FontGreen="\033[32m";
+FontYellow="\033[33m";
+FontBlue="\033[34m";
+FontPurple="\033[35m";
+FontSkyBlue="\033[36m";
+FontWhite="\033[37m";
+FontSuffix="\033[0m";
+
+get_latest_version() {
+    # Get latest release version number
+    local latest_release
+    if ! latest_release=$(curl -x "${PROXY}" -sS -H "Accept: application/vnd.github.v3+json" "https://api.github.com/repos/0xJacky/nginx-ui/releases/latest"); then
+        echo -e "${FontRed}error: Failed to get release list, please check your network.${FontSuffix}"
+        exit 1
+    fi
+
+    RELEASE_LATEST="$(echo "$latest_release" | sed 'y/,/\n/' | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')"
+    if [[ -z "$RELEASE_LATEST" ]]; then
+        if echo "$latest_release" | grep -q "API rate limit exceeded"; then
+            echo -e "${FontRed}error: github API rate limit exceeded${FontSuffix}"
+        else
+            echo -e "${FontRed}error: Failed to get the latest release version.${FontSuffix}"
+            echo "Welcome bug report: https://github.com/0xJacky/nginx-ui/issues"
+        fi
+        exit 1
+    fi
+    RELEASE_LATEST="v${RELEASE_LATEST#v}"
+}
+
+download_nginx_ui() {
+    local download_link
+    download_link="${RPROXY}https://github.com/0xJacky/nginx-ui/releases/download/$RELEASE_LATEST/nginx-ui-linux-$MACHINE.tar.gz"
+
+    echo "Downloading Nginx UI archive: $download_link"
+    if ! curl -x "${PROXY}" -R -H 'Cache-Control: no-cache' -L -o "$TAR_FILE" "$download_link"; then
+        echo 'error: Download failed! Please check your network or try again.'
+        return 1
+    fi
+    return 0
+}
+
+decompression() {
+    echo "$1"
+    if ! tar -zxf "$1" -C "$TMP_DIRECTORY"; then
+        echo -e "${FontRed}error: Nginx UI decompression failed.${FontSuffix}"
+        "rm" -r "$TMP_DIRECTORY"
+        echo "removed: $TMP_DIRECTORY"
+        exit 1
+    fi
+    echo "info: Extract the Nginx UI package to $TMP_DIRECTORY and prepare it for installation."
+}
+
+install_bin() {
+    NAME="nginx-ui"
+    install -m 755 "${TMP_DIRECTORY}/$NAME" "/app/$NAME"
+}
+
+main() {
+    # Important Variables
+    TMP_DIRECTORY="$(mktemp -d)"
+    TAR_FILE="${TMP_DIRECTORY}/nginx-ui-linux-$MACHINE.tar.gz"
+    get_latest_version
+    download_nginx_ui
+    decompression "$TAR_FILE"
+    install_bin
+}
+
+main "$@"

+ 16 - 0
resources/demo/nginx.conf

@@ -0,0 +1,16 @@
+server {
+    listen       80;
+    server_name  localhost;  # your domain here
+    client_max_body_size 128M;  # maximum upload size
+
+    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 upgrade;
+        proxy_pass http://127.0.0.1:9000/;
+    }
+}

+ 4 - 0
resources/demo/sources.list

@@ -0,0 +1,4 @@
+deb http://mirrors.aliyun.com/debian/ buster main non-free contrib
+deb http://mirrors.aliyun.com/debian-security buster/updates main
+deb http://mirrors.aliyun.com/debian/ buster-updates main non-free contrib
+deb http://mirrors.aliyun.com/debian/ buster-backports main non-free contrib

+ 3 - 0
resources/demo/start.sh

@@ -0,0 +1,3 @@
+#!/bin/bash
+nginx
+/app/nginx-ui --config app.ini

+ 151 - 138
server/api/cert.go

@@ -1,150 +1,163 @@
 package api
 package api
 
 
 import (
 import (
-	"encoding/json"
-	"github.com/0xJacky/Nginx-UI/server/tool"
-	"github.com/gin-gonic/gin"
-	"github.com/gorilla/websocket"
-	"log"
-	"net/http"
-	"os"
+    "encoding/json"
+    "github.com/0xJacky/Nginx-UI/server/settings"
+    "github.com/0xJacky/Nginx-UI/server/tool"
+    "github.com/gin-gonic/gin"
+    "github.com/gorilla/websocket"
+    "log"
+    "net/http"
+    "os"
 )
 )
 
 
 func CertInfo(c *gin.Context) {
 func CertInfo(c *gin.Context) {
-	domain := c.Param("domain")
+    domain := c.Param("domain")
 
 
-	key := tool.GetCertInfo(domain)
+    key := tool.GetCertInfo(domain)
 
 
-	c.JSON(http.StatusOK, gin.H{
-		"subject_name": key.Subject.CommonName,
-		"issuer_name":  key.Issuer.CommonName,
-		"not_after":    key.NotAfter,
-		"not_before":   key.NotBefore,
-	})
+    c.JSON(http.StatusOK, gin.H{
+        "subject_name": key.Subject.CommonName,
+        "issuer_name":  key.Issuer.CommonName,
+        "not_after":    key.NotAfter,
+        "not_before":   key.NotBefore,
+    })
 }
 }
 
 
 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
-		},
-	}
-
-	// 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(err)
-			return
-		}
-	}(ws)
-
-	for {
-		// read
-		mt, message, err := ws.ReadMessage()
-		if err != nil {
-			break
-		}
-		if string(message) == "go" {
-			var m []byte
-
-			err = tool.IssueCert(domain)
-			if err != nil {
-				m, err = json.Marshal(gin.H{
-					"status":  "error",
-					"message": err.Error(),
-				})
-
-				if err != nil {
-					log.Println(err)
-					return
-				}
-
-				err = ws.WriteMessage(mt, m)
-
-				if err != nil {
-					log.Println(err)
-					return
-				}
-
-				log.Println(err)
-				return
-			}
-
-			sslCertificatePath := tool.GetNginxConfPath("ssl/" + domain + "/fullchain.cer")
-			_, err = os.Stat(sslCertificatePath)
-
-			if err != nil {
-				log.Println(err)
-				return
-			}
-
-			log.Println("[found]", "fullchain.cer")
-			m, err = json.Marshal(gin.H{
-				"status":  "success",
-				"message": "[found] fullchain.cer",
-			})
-
-			if err != nil {
-				log.Println(err)
-				return
-			}
-
-			err = ws.WriteMessage(mt, m)
-
-			if err != nil {
-				log.Println(err)
-				return
-			}
-
-			sslCertificateKeyPath := tool.GetNginxConfPath("ssl/" + domain + "/" + domain + ".key")
-			_, err = os.Stat(sslCertificateKeyPath)
-
-			if err != nil {
-				log.Println(err)
-				return
-			}
-
-			log.Println("[found]", "cert key")
-			m, err = json.Marshal(gin.H{
-				"status":  "success",
-				"message": "[found] cert key",
-			})
-
-			if err != nil {
-				log.Println(err)
-			}
-
-			err = ws.WriteMessage(mt, m)
-
-			if err != nil {
-				log.Println(err)
-			}
-
-			log.Println("申请成功")
-			m, err = json.Marshal(gin.H{
-				"status":              "success",
-				"message":             "申请成功",
-				"ssl_certificate":     sslCertificatePath,
-				"ssl_certificate_key": sslCertificateKeyPath,
-			})
-
-			if err != nil {
-				log.Println(err)
-			}
-
-			err = ws.WriteMessage(mt, m)
-
-			if err != nil {
-				log.Println(err)
-			}
-		}
-	}
+    domain := c.Param("domain")
+    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
+    }
+
+    defer func(ws *websocket.Conn) {
+        err := ws.Close()
+        if err != nil {
+            log.Println(err)
+            return
+        }
+    }(ws)
+
+    for {
+        // read
+        mt, message, err := ws.ReadMessage()
+        if err != nil {
+            break
+        }
+        if string(message) == "go" {
+            var m []byte
+
+            if settings.ServerSettings.Demo {
+                m, _ = json.Marshal(gin.H{
+                    "status":  "error",
+                    "message": "this feature is not available in demo",
+                })
+                _ = ws.WriteMessage(mt, m)
+                return
+            }
+
+            err = tool.IssueCert(domain)
+
+            if err != nil {
+
+                log.Println(err)
+
+                m, err = json.Marshal(gin.H{
+                    "status":  "error",
+                    "message": err.Error(),
+                })
+
+                if err != nil {
+                    log.Println(err)
+                    return
+                }
+
+                err = ws.WriteMessage(mt, m)
+
+                if err != nil {
+                    log.Println(err)
+                    return
+                }
+
+                return
+            }
+
+            sslCertificatePath := tool.GetNginxConfPath("ssl/" + domain + "/fullchain.cer")
+            _, err = os.Stat(sslCertificatePath)
+
+            if err != nil {
+                log.Println(err)
+                return
+            }
+
+            log.Println("[found]", "fullchain.cer")
+            m, err = json.Marshal(gin.H{
+                "status":  "success",
+                "message": "[found] fullchain.cer",
+            })
+
+            if err != nil {
+                log.Println(err)
+                return
+            }
+
+            err = ws.WriteMessage(mt, m)
+
+            if err != nil {
+                log.Println(err)
+                return
+            }
+
+            sslCertificateKeyPath := tool.GetNginxConfPath("ssl/" + domain + "/" + domain + ".key")
+            _, err = os.Stat(sslCertificateKeyPath)
+
+            if err != nil {
+                log.Println(err)
+                return
+            }
+
+            log.Println("[found]", "cert key")
+            m, err = json.Marshal(gin.H{
+                "status":  "success",
+                "message": "[found] cert key",
+            })
+
+            if err != nil {
+                log.Println(err)
+            }
+
+            err = ws.WriteMessage(mt, m)
+
+            if err != nil {
+                log.Println(err)
+            }
+
+            log.Println("申请成功")
+            m, err = json.Marshal(gin.H{
+                "status":              "success",
+                "message":             "申请成功",
+                "ssl_certificate":     sslCertificatePath,
+                "ssl_certificate_key": sslCertificateKeyPath,
+            })
+
+            if err != nil {
+                log.Println(err)
+            }
+
+            err = ws.WriteMessage(mt, m)
+
+            if err != nil {
+                log.Println(err)
+            }
+        }
+    }
 }
 }

+ 2 - 4
server/api/template.go

@@ -2,18 +2,16 @@ package api
 
 
 import (
 import (
 	"github.com/0xJacky/Nginx-UI/server/settings"
 	"github.com/0xJacky/Nginx-UI/server/settings"
+	"github.com/0xJacky/Nginx-UI/server/template"
 	"github.com/gin-gonic/gin"
 	"github.com/gin-gonic/gin"
-	"io/ioutil"
 	"net/http"
 	"net/http"
 	"os"
 	"os"
-	"path/filepath"
 	"strings"
 	"strings"
 )
 )
 
 
 func GetTemplate(c *gin.Context) {
 func GetTemplate(c *gin.Context) {
 	name := c.Param("name")
 	name := c.Param("name")
-	path := filepath.Join("template", name)
-	content, err := ioutil.ReadFile(path)
+	content, err := template.DistFS.ReadFile(name)
 
 
 	_content := string(content)
 	_content := string(content)
 	_content = strings.ReplaceAll(_content, "{{ HTTP01PORT }}",
 	_content = strings.ReplaceAll(_content, "{{ HTTP01PORT }}",

+ 78 - 70
server/router/routers.go

@@ -1,77 +1,85 @@
 package router
 package router
 
 
 import (
 import (
-	"bufio"
-	"github.com/0xJacky/Nginx-UI/server/api"
-	"github.com/gin-contrib/static"
-	"github.com/gin-gonic/gin"
-	"net/http"
-	"strings"
+    "bufio"
+    "github.com/0xJacky/Nginx-UI/server/api"
+    "github.com/0xJacky/Nginx-UI/server/settings"
+    "github.com/gin-contrib/static"
+    "github.com/gin-gonic/gin"
+    "net/http"
+    "strings"
 )
 )
 
 
 func InitRouter() *gin.Engine {
 func InitRouter() *gin.Engine {
-	r := gin.New()
-	r.Use(gin.Logger())
-
-	r.Use(gin.Recovery())
-
-	r.Use(static.Serve("/", mustFS("")))
-
-	r.NoRoute(func(c *gin.Context) {
-		accept := c.Request.Header.Get("Accept")
-		if strings.Contains(accept, "text/html") {
-			file, _ := mustFS("").Open("index.html")
-			stat, _ := file.Stat()
-			c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
-				bufio.NewReader(file), nil)
-		}
-	})
-
-	g := r.Group("/api")
-	{
-		g.GET("install", api.InstallLockCheck)
-		g.POST("install", api.InstallNginxUI)
-
-		g.POST("/login", api.Login)
-		g.DELETE("/logout", api.Logout)
-
-		g := g.Group("/", authRequired())
-		{
-			g.GET("/analytic", api.Analytic)
-			g.GET("/analytic/init", api.GetAnalyticInit)
-
-			g.GET("/users", api.GetUsers)
-			g.GET("/user/:id", api.GetUser)
-			g.POST("/user", api.AddUser)
-			g.POST("/user/:id", api.EditUser)
-			g.DELETE("/user/:id", api.DeleteUser)
-
-			g.GET("domains", api.GetDomains)
-			g.GET("domain/:name", api.GetDomain)
-			g.POST("domain/:name", api.EditDomain)
-			g.POST("domain/:name/enable", api.EnableDomain)
-			g.POST("domain/:name/disable", api.DisableDomain)
-			g.DELETE("domain/:name", api.DeleteDomain)
-
-			g.GET("configs", api.GetConfigs)
-			g.GET("config/:name", api.GetConfig)
-			g.POST("config", api.AddConfig)
-			g.POST("config/:name", api.EditConfig)
-
-			g.GET("backups", api.GetFileBackupList)
-			g.GET("backup/:id", api.GetFileBackup)
-
-			g.GET("template/:name", api.GetTemplate)
-
-			g.GET("cert/issue/:domain", api.IssueCert)
-			g.GET("cert/:domain/info", api.CertInfo)
-
-			// 添加域名到自动续期列表
-			g.POST("cert/:domain", api.AddDomainToAutoCert)
-			// 从自动续期列表中删除域名
-			g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
-		}
-	}
-
-	return r
+    r := gin.New()
+    r.Use(gin.Logger())
+
+    r.Use(gin.Recovery())
+
+    r.Use(static.Serve("/", mustFS("")))
+
+    r.NoRoute(func(c *gin.Context) {
+        accept := c.Request.Header.Get("Accept")
+        if strings.Contains(accept, "text/html") {
+            file, _ := mustFS("").Open("index.html")
+            stat, _ := file.Stat()
+            c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
+                bufio.NewReader(file), nil)
+        }
+    })
+
+    g := r.Group("/api")
+    {
+
+        g.GET("settings", func(c *gin.Context) {
+            c.JSON(http.StatusOK, gin.H{
+                "demo": settings.ServerSettings.Demo,
+            })
+        })
+
+        g.GET("install", api.InstallLockCheck)
+        g.POST("install", api.InstallNginxUI)
+
+        g.POST("/login", api.Login)
+        g.DELETE("/logout", api.Logout)
+
+        g := g.Group("/", authRequired())
+        {
+            g.GET("/analytic", api.Analytic)
+            g.GET("/analytic/init", api.GetAnalyticInit)
+
+            g.GET("/users", api.GetUsers)
+            g.GET("/user/:id", api.GetUser)
+            g.POST("/user", api.AddUser)
+            g.POST("/user/:id", api.EditUser)
+            g.DELETE("/user/:id", api.DeleteUser)
+
+            g.GET("domains", api.GetDomains)
+            g.GET("domain/:name", api.GetDomain)
+            g.POST("domain/:name", api.EditDomain)
+            g.POST("domain/:name/enable", api.EnableDomain)
+            g.POST("domain/:name/disable", api.DisableDomain)
+            g.DELETE("domain/:name", api.DeleteDomain)
+
+            g.GET("configs", api.GetConfigs)
+            g.GET("config/:name", api.GetConfig)
+            g.POST("config", api.AddConfig)
+            g.POST("config/:name", api.EditConfig)
+
+            g.GET("backups", api.GetFileBackupList)
+            g.GET("backup/:id", api.GetFileBackup)
+
+            g.GET("template/:name", api.GetTemplate)
+
+            g.GET("cert/issue/:domain", api.IssueCert)
+            g.GET("cert/:domain/info", api.CertInfo)
+
+            // 添加域名到自动续期列表
+            g.POST("cert/:domain", api.AddDomainToAutoCert)
+            // 从自动续期列表中删除域名
+            g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
+        }
+    }
+
+    return r
 }
 }

+ 2 - 0
server/settings/settings.go

@@ -15,6 +15,7 @@ type Server struct {
 	HTTPChallengePort string
 	HTTPChallengePort string
 	Email             string
 	Email             string
 	Database          string
 	Database          string
+	Demo              bool
 }
 }
 
 
 var ServerSettings = &Server{
 var ServerSettings = &Server{
@@ -22,6 +23,7 @@ var ServerSettings = &Server{
 	RunMode:           "debug",
 	RunMode:           "debug",
 	HTTPChallengePort: "9180",
 	HTTPChallengePort: "9180",
 	Database:          "database",
 	Database:          "database",
+	Demo:              false,
 }
 }
 
 
 var ConfPath string
 var ConfPath string

+ 6 - 0
server/template/template.go

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

+ 2 - 2
server/test/lego_test.go

@@ -48,8 +48,8 @@ func TestLego(t *testing.T) {
 
 
 	config := lego.NewConfig(&myUser)
 	config := lego.NewConfig(&myUser)
 
 
-	// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
-	//config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
+	// This CA URL is configured for a local dev instance of Boulder running in Dockerfile in a VM.
+	config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
 	config.Certificate.KeyType = certcrypto.RSA2048
 	config.Certificate.KeyType = certcrypto.RSA2048
 
 
 	// A client facilitates communication with the CA server.
 	// A client facilitates communication with the CA server.

+ 141 - 139
server/tool/cert.go

@@ -1,169 +1,171 @@
 package tool
 package tool
 
 
 import (
 import (
-	"crypto"
-	"crypto/ecdsa"
-	"crypto/elliptic"
-	"crypto/rand"
-	"crypto/tls"
-	"crypto/x509"
-	"github.com/0xJacky/Nginx-UI/server/model"
-	"github.com/0xJacky/Nginx-UI/server/settings"
-	"github.com/go-acme/lego/v4/certcrypto"
-	"github.com/go-acme/lego/v4/certificate"
-	"github.com/go-acme/lego/v4/challenge/http01"
-	"github.com/go-acme/lego/v4/lego"
-	"github.com/go-acme/lego/v4/registration"
-	"io"
-	"io/ioutil"
-	"log"
-	"net/http"
-	"os"
-	"path/filepath"
-	"time"
+    "crypto"
+    "crypto/ecdsa"
+    "crypto/elliptic"
+    "crypto/rand"
+    "crypto/tls"
+    "crypto/x509"
+    "github.com/0xJacky/Nginx-UI/server/model"
+    "github.com/0xJacky/Nginx-UI/server/settings"
+    "github.com/go-acme/lego/v4/certcrypto"
+    "github.com/go-acme/lego/v4/certificate"
+    "github.com/go-acme/lego/v4/challenge/http01"
+    "github.com/go-acme/lego/v4/lego"
+    "github.com/go-acme/lego/v4/registration"
+    "io"
+    "io/ioutil"
+    "log"
+    "net/http"
+    "os"
+    "path/filepath"
+    "time"
 )
 )
 
 
 // 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
 type MyUser struct {
 type MyUser struct {
-	Email        string
-	Registration *registration.Resource
-	key          crypto.PrivateKey
+    Email        string
+    Registration *registration.Resource
+    key          crypto.PrivateKey
 }
 }
 
 
 func (u *MyUser) GetEmail() string {
 func (u *MyUser) GetEmail() string {
-	return u.Email
+    return u.Email
 }
 }
 func (u MyUser) GetRegistration() *registration.Resource {
 func (u MyUser) GetRegistration() *registration.Resource {
-	return u.Registration
+    return u.Registration
 }
 }
 func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
 func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
-	return u.key
+    return u.key
 }
 }
 
 
 func AutoCert() {
 func AutoCert() {
-	for {
-		log.Println("[AutoCert] Start")
-		autoCertList := model.GetAutoCertList()
-		for i := range autoCertList {
-			domain := autoCertList[i].Domain
-			key := GetCertInfo(domain)
-			// 未到一个月
-			if time.Now().Before(key.NotBefore.AddDate(0, 1, 0)) {
-				continue
-			}
-			// 过一个月了
-			err := IssueCert(domain)
-			if err != nil {
-				log.Println(err)
-			}
-		}
-		time.Sleep(1 * time.Hour)
-	}
+    for {
+        log.Println("[AutoCert] Start")
+        autoCertList := model.GetAutoCertList()
+        for i := range autoCertList {
+            domain := autoCertList[i].Domain
+            key := GetCertInfo(domain)
+            // 未到一个月
+            if time.Now().Before(key.NotBefore.AddDate(0, 1, 0)) {
+                continue
+            }
+            // 过一个月了
+            err := IssueCert(domain)
+            if err != nil {
+                log.Println(err)
+            }
+        }
+        time.Sleep(1 * time.Hour)
+    }
 }
 }
 
 
 func GetCertInfo(domain string) (key *x509.Certificate) {
 func GetCertInfo(domain string) (key *x509.Certificate) {
-	ts := &http.Transport{
-		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
-	}
+    ts := &http.Transport{
+        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+    }
 
 
-	client := &http.Client{Transport: ts}
+    client := &http.Client{Transport: ts}
 
 
-	response, err := client.Get("https://" + domain)
+    response, err := client.Get("https://" + domain)
 
 
-	if err != nil {
-		return
-	}
+    if err != nil {
+        return
+    }
 
 
-	defer func(Body io.ReadCloser) {
-		err = Body.Close()
-		if err != nil {
-			log.Println(err)
-			return
-		}
-	}(response.Body)
+    defer func(Body io.ReadCloser) {
+        err = Body.Close()
+        if err != nil {
+            log.Println(err)
+            return
+        }
+    }(response.Body)
 
 
-	key = response.TLS.PeerCertificates[0]
+    key = response.TLS.PeerCertificates[0]
 
 
-	return
+    return
 }
 }
 
 
 func IssueCert(domain string) error {
 func IssueCert(domain string) error {
-	// Create a user. New accounts need an email and private key to start.
-	privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
-	if err != nil {
-		log.Println(err)
-		return err
-	}
-
-	myUser := MyUser{
-		Email: settings.ServerSettings.Email,
-		key:   privateKey,
-	}
-
-	config := lego.NewConfig(&myUser)
-
-	//config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
-	config.Certificate.KeyType = certcrypto.RSA2048
-
-	// A client facilitates communication with the CA server.
-	client, err := lego.NewClient(config)
-	if err != nil {
-		log.Println(err)
-		return err
-	}
-
-	err = client.Challenge.SetHTTP01Provider(
-		http01.NewProviderServer("",
-			settings.ServerSettings.HTTPChallengePort,
-		),
-	)
-	if err != nil {
-		log.Println(err)
-		return err
-	}
-
-	// New users will need to register
-	reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
-	if err != nil {
-		log.Println(err)
-		return err
-	}
-	myUser.Registration = reg
-
-	request := certificate.ObtainRequest{
-		Domains: []string{domain},
-		Bundle:  true,
-	}
-	certificates, err := client.Certificate.Obtain(request)
-	if err != nil {
-		log.Println(err)
-		return err
-	}
-	saveDir := GetNginxConfPath("ssl/" + domain)
-	if _, err := os.Stat(saveDir); os.IsNotExist(err) {
-		err = os.Mkdir(saveDir, 0755)
-		if err != nil {
-			log.Println("fail to create", saveDir)
-			return err
-		}
-	}
-
-	// Each certificate comes back with the cert bytes, the bytes of the client's
-	// private key, and a certificate URL. SAVE THESE TO DISK.
-	err = ioutil.WriteFile(filepath.Join(saveDir, "fullchain.cer"),
-		certificates.Certificate, 0644)
-	if err != nil {
-		log.Println(err)
-		return err
-	}
-	err = ioutil.WriteFile(filepath.Join(saveDir, domain+".key"),
-		certificates.PrivateKey, 0644)
-	if err != nil {
-		log.Println(err)
-		return err
-	}
-
-	ReloadNginx()
-
-	return nil
+    // Create a user. New accounts need an email and private key to start.
+    privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+    if err != nil {
+        log.Println(err)
+        return err
+    }
+
+    myUser := MyUser{
+        Email: settings.ServerSettings.Email,
+        key:   privateKey,
+    }
+
+    config := lego.NewConfig(&myUser)
+
+    if settings.ServerSettings.Demo {
+        config.CADirURL = "https://acme-staging-v02.api.letsencrypt.org/directory"
+    }
+    config.Certificate.KeyType = certcrypto.RSA2048
+
+    // A client facilitates communication with the CA server.
+    client, err := lego.NewClient(config)
+    if err != nil {
+        log.Println(err)
+        return err
+    }
+
+    err = client.Challenge.SetHTTP01Provider(
+        http01.NewProviderServer("",
+            settings.ServerSettings.HTTPChallengePort,
+        ),
+    )
+    if err != nil {
+        log.Println(err)
+        return err
+    }
+
+    // New users will need to register
+    reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
+    if err != nil {
+        log.Println(err)
+        return err
+    }
+    myUser.Registration = reg
+
+    request := certificate.ObtainRequest{
+        Domains: []string{domain},
+        Bundle:  true,
+    }
+    certificates, err := client.Certificate.Obtain(request)
+    if err != nil {
+        log.Println(err)
+        return err
+    }
+    saveDir := GetNginxConfPath("ssl/" + domain)
+    if _, err := os.Stat(saveDir); os.IsNotExist(err) {
+        err = os.Mkdir(saveDir, 0755)
+        if err != nil {
+            log.Println("fail to create", saveDir)
+            return err
+        }
+    }
+
+    // Each certificate comes back with the cert bytes, the bytes of the client's
+    // private key, and a certificate URL. SAVE THESE TO DISK.
+    err = ioutil.WriteFile(filepath.Join(saveDir, "fullchain.cer"),
+        certificates.Certificate, 0644)
+    if err != nil {
+        log.Println(err)
+        return err
+    }
+    err = ioutil.WriteFile(filepath.Join(saveDir, domain+".key"),
+        certificates.PrivateKey, 0644)
+    if err != nil {
+        log.Println(err)
+        return err
+    }
+
+    ReloadNginx()
+
+    return nil
 }
 }

部分文件因文件數量過多而無法顯示