From 175d19a206b19a94fef7ecbd23143c8fdc2f63de Mon Sep 17 00:00:00 2001
From: 0xJacky
Date: Wed, 15 Feb 2023 10:44:30 +0800
Subject: [PATCH 1/6] fix: server_name split issue in SAN cert
---
.../components/StdDataDisplay/StdTable.vue | 2 +-
frontend/src/views/dashboard/DashBoard.vue | 6 +-
frontend/src/views/domain/DomainEdit.vue | 2 +-
frontend/src/views/domain/cert/Cert.vue | 3 +-
frontend/src/views/domain/cert/IssueCert.vue | 14 +-
.../views/domain/ngx_conf/NgxConfigEditor.vue | 1 +
frontend/vite.config.ts | 2 +-
go.mod | 1 +
go.sum | 2 +
main.go | 2 +-
server/api/cert.go | 37 +-
server/api/domain.go | 737 +++++++++---------
server/model/cert.go | 32 +-
server/pkg/cert/auto_cert.go | 24 +-
server/pkg/cert/cert.go | 267 ++++---
server/pkg/nginx/type.go | 63 +-
server/router/routers.go | 7 +-
17 files changed, 601 insertions(+), 601 deletions(-)
diff --git a/frontend/src/components/StdDataDisplay/StdTable.vue b/frontend/src/components/StdDataDisplay/StdTable.vue
index f7611d4ed..1d64e96b5 100644
--- a/frontend/src/components/StdDataDisplay/StdTable.vue
+++ b/frontend/src/components/StdDataDisplay/StdTable.vue
@@ -525,7 +525,7 @@ function initSortable() {
:okText="$gettext('OK')"
:title="$gettext('Are you sure you want to delete?')"
@confirm="destroy(record[rowKey])">
- Delete
+ {{ $gettext('Delete') }}
diff --git a/frontend/src/views/dashboard/DashBoard.vue b/frontend/src/views/dashboard/DashBoard.vue
index c535270f9..e2487c20f 100644
--- a/frontend/src/views/dashboard/DashBoard.vue
+++ b/frontend/src/views/dashboard/DashBoard.vue
@@ -164,7 +164,7 @@ function wsOnMessage(m: { data: any }) {
{{ $gettext('CPU:') + ' ' }}
- {{ cpu_info[0]?.modelName }}
+ {{ cpu_info[0]?.modelName || 'core' }}
{{ (cpu_info[0]?.mhz / 1000).toFixed(2) + 'GHz' }}
* {{ cpu_info.length }}
@@ -303,10 +303,6 @@ function wsOnMessage(m: { data: any }) {
}
}
-.os-platform {
- text-transform: capitalize;
-}
-
.load-avg-describe {
@media (max-width: 1600px) and (min-width: 1200px) {
display: none;
diff --git a/frontend/src/views/domain/DomainEdit.vue b/frontend/src/views/domain/DomainEdit.vue
index 6d9fe6290..564a7ddb5 100644
--- a/frontend/src/views/domain/DomainEdit.vue
+++ b/frontend/src/views/domain/DomainEdit.vue
@@ -25,7 +25,7 @@ watch(route, () => {
const update = ref(0)
const ngx_config = reactive({
- filename: '',
+ name: '',
upstreams: [],
servers: []
})
diff --git a/frontend/src/views/domain/cert/Cert.vue b/frontend/src/views/domain/cert/Cert.vue
index d32c51cdd..f559432dd 100644
--- a/frontend/src/views/domain/cert/Cert.vue
+++ b/frontend/src/views/domain/cert/Cert.vue
@@ -7,7 +7,7 @@ import ChangeCert from '@/views/domain/cert/ChangeCert.vue'
const {$gettext} = useGettext()
-const props = defineProps(['directivesMap', 'current_server_directives', 'enabled', 'cert_info'])
+const props = defineProps(['config_name', 'directivesMap', 'current_server_directives', 'enabled', 'cert_info'])
const emit = defineEmits(['callback', 'update:enabled'])
@@ -38,6 +38,7 @@ const enabled = computed({
import {useGettext} from 'vue3-gettext'
-import {computed, h, nextTick, onMounted, ref, VNode, watch} from 'vue'
+import {computed, nextTick, ref, watch} from 'vue'
import {message} from 'ant-design-vue'
import domain from '@/api/domain'
import websocket from '@/lib/websocket'
@@ -8,7 +8,7 @@ import Template from '@/views/template/Template.vue'
const {$gettext, interpolate} = useGettext()
-const props = defineProps(['directivesMap', 'current_server_directives', 'enabled'])
+const props = defineProps(['config_name', 'directivesMap', 'current_server_directives', 'enabled'])
const emit = defineEmits(['changeEnabled', 'callback', 'update:enabled'])
@@ -50,7 +50,7 @@ function job() {
})
}
}).then(() => {
- issue_cert(name.value, callback)
+ issue_cert(props.config_name, name.value, callback)
})
}
@@ -61,13 +61,13 @@ function callback(ssl_certificate: string, ssl_certificate_key: string) {
function change_auto_cert(r: boolean) {
if (r) {
- domain.add_auto_cert(name.value).then(() => {
+ domain.add_auto_cert(props.config_name).then(() => {
message.success(interpolate($gettext('Auto-renewal enabled for %{name}'), {name: name.value}))
}).catch(e => {
message.error(e.message ?? interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: name.value}))
})
} else {
- domain.remove_auto_cert(name.value).then(() => {
+ domain.remove_auto_cert(props.config_name).then(() => {
message.success(interpolate($gettext('Auto-renewal disabled for %{name}'), {name: name.value}))
}).catch(e => {
message.error(e.message ?? interpolate($gettext('Disable auto-renewal failed for %{name}'), {name: name.value}))
@@ -86,7 +86,7 @@ function log(msg: string) {
(logContainer.value as any as Element).scroll({top: 320, left: 0, behavior: 'smooth'})
}
-const issue_cert = async (server_name: string, callback: Function) => {
+const issue_cert = async (config_name: string, server_name: string, callback: Function) => {
progressStatus.value = 'active'
modalClosable.value = false
modalVisible.value = true
@@ -95,7 +95,7 @@ const issue_cert = async (server_name: string, callback: Function) => {
log($gettext('Getting the certificate, please wait...'))
- const ws = websocket('/api/cert/issue', false)
+ const ws = websocket(`/api/domain/${config_name}/cert`, false)
ws.onopen = () => {
ws.send(JSON.stringify({
diff --git a/frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue b/frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue
index 1447162ec..ef950c8ca 100644
--- a/frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue
+++ b/frontend/src/views/domain/ngx_conf/NgxConfigEditor.vue
@@ -168,6 +168,7 @@ watch(current_server_index, () => {
= nginx.Warn {
- c.JSON(http.StatusInternalServerError, gin.H{
- "message": output,
- "error": "nginx_config_syntax_error",
- })
- return
- }
-
- output = nginx.Reload()
-
- if nginx.GetLogLevel(output) >= nginx.Warn {
- c.JSON(http.StatusInternalServerError, gin.H{
- "message": output,
- })
- return
- }
- }
-
- GetDomain(c)
+ name := c.Param("name")
+
+ 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" binding:"required"`
+ Overwrite bool `json:"overwrite"`
+ }
+
+ if !BindAndValid(c, &json) {
+ return
+ }
+
+ path := nginx.GetConfPath("sites-available", name)
+
+ if !json.Overwrite && helper.FileExists(path) {
+ c.JSON(http.StatusNotAcceptable, gin.H{
+ "message": "File exists",
+ })
+ return
+ }
+
+ err := os.WriteFile(path, []byte(json.Content), 0644)
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+ enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
+ // rename the config file if needed
+ if name != json.Name {
+ newPath := nginx.GetConfPath("sites-available", json.Name)
+ // check if dst file exists, do not rename
+ if helper.FileExists(newPath) {
+ c.JSON(http.StatusNotAcceptable, gin.H{
+ "message": "File exists",
+ })
+ return
+ }
+ // recreate soft link
+ if helper.FileExists(enabledConfigFilePath) {
+ _ = os.Remove(enabledConfigFilePath)
+ enabledConfigFilePath = nginx.GetConfPath("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 = nginx.GetConfPath("sites-enabled", name)
+ if helper.FileExists(enabledConfigFilePath) {
+ // Test nginx configuration
+ output := nginx.TestConf()
+ if nginx.GetLogLevel(output) >= nginx.Warn {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "message": output,
+ "error": "nginx_config_syntax_error",
+ })
+ return
+ }
+
+ output = nginx.Reload()
+
+ if nginx.GetLogLevel(output) >= nginx.Warn {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "message": output,
+ })
+ return
+ }
+ }
+
+ GetDomain(c)
}
func EnableDomain(c *gin.Context) {
- configFilePath := nginx.GetConfPath("sites-available", c.Param("name"))
- enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
-
- _, err := os.Stat(configFilePath)
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
- err = os.Symlink(configFilePath, enabledConfigFilePath)
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
- }
-
- // Test nginx config, if not pass then disable the site.
- output := nginx.TestConf()
-
- if nginx.GetLogLevel(output) >= nginx.Warn {
- _ = os.Remove(enabledConfigFilePath)
- c.JSON(http.StatusInternalServerError, gin.H{
- "message": output,
- })
- return
- }
-
- output = nginx.Reload()
-
- if nginx.GetLogLevel(output) >= nginx.Warn {
- c.JSON(http.StatusInternalServerError, gin.H{
- "message": output,
- })
- return
- }
-
- c.JSON(http.StatusOK, gin.H{
- "message": "ok",
- })
+ configFilePath := nginx.GetConfPath("sites-available", c.Param("name"))
+ enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
+
+ _, err := os.Stat(configFilePath)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
+ err = os.Symlink(configFilePath, enabledConfigFilePath)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+ }
+
+ // Test nginx config, if not pass then disable the site.
+ output := nginx.TestConf()
+
+ if nginx.GetLogLevel(output) >= nginx.Warn {
+ _ = os.Remove(enabledConfigFilePath)
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "message": output,
+ })
+ return
+ }
+
+ output = nginx.Reload()
+
+ if nginx.GetLogLevel(output) >= nginx.Warn {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "message": output,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "ok",
+ })
}
func DisableDomain(c *gin.Context) {
- enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
-
- _, err := os.Stat(enabledConfigFilePath)
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- err = os.Remove(enabledConfigFilePath)
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- // delete auto cert record
- certModel := model.Cert{Domain: c.Param("name")}
- err = certModel.Remove()
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- output := nginx.Reload()
-
- if nginx.GetLogLevel(output) >= nginx.Warn {
- c.JSON(http.StatusInternalServerError, gin.H{
- "message": output,
- })
- return
- }
-
- c.JSON(http.StatusOK, gin.H{
- "message": "ok",
- })
+ enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
+
+ _, err := os.Stat(enabledConfigFilePath)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ err = os.Remove(enabledConfigFilePath)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ // delete auto cert record
+ certModel := model.Cert{Filename: c.Param("name")}
+ err = certModel.Remove()
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ output := nginx.Reload()
+
+ if nginx.GetLogLevel(output) >= nginx.Warn {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "message": output,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "ok",
+ })
}
func DeleteDomain(c *gin.Context) {
- var err error
- name := c.Param("name")
- availablePath := nginx.GetConfPath("sites-available", name)
- enabledPath := nginx.GetConfPath("sites-enabled", name)
-
- if _, err = os.Stat(availablePath); os.IsNotExist(err) {
- c.JSON(http.StatusNotFound, gin.H{
- "message": "site not found",
- })
- return
- }
-
- if _, err = os.Stat(enabledPath); err == nil {
- c.JSON(http.StatusNotAcceptable, gin.H{
- "message": "site is enabled",
- })
- return
- }
-
- certModel := model.Cert{Domain: name}
- _ = certModel.Remove()
-
- err = os.Remove(availablePath)
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- c.JSON(http.StatusOK, gin.H{
- "message": "ok",
- })
+ var err error
+ name := c.Param("name")
+ availablePath := nginx.GetConfPath("sites-available", name)
+ enabledPath := nginx.GetConfPath("sites-enabled", name)
+
+ if _, err = os.Stat(availablePath); os.IsNotExist(err) {
+ c.JSON(http.StatusNotFound, gin.H{
+ "message": "site not found",
+ })
+ return
+ }
+
+ if _, err = os.Stat(enabledPath); err == nil {
+ c.JSON(http.StatusNotAcceptable, gin.H{
+ "message": "site is enabled",
+ })
+ return
+ }
+
+ certModel := model.Cert{Filename: name}
+ _ = certModel.Remove()
+
+ err = os.Remove(availablePath)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "ok",
+ })
}
func AddDomainToAutoCert(c *gin.Context) {
- domain := c.Param("domain")
- domain = strings.ReplaceAll(domain, " ", "_")
- certModel, err := model.FirstOrCreateCert(domain)
+ name := c.Param("name")
+ certModel, err := model.FirstOrCreateCert(name)
- if err != nil {
- ErrHandler(c, err)
- return
- }
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
- err = certModel.Updates(&model.Cert{
- AutoCert: model.AutoCertEnabled,
- })
+ err = certModel.Updates(&model.Cert{
+ AutoCert: model.AutoCertEnabled,
+ })
- if err != nil {
- ErrHandler(c, err)
- return
- }
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
- c.JSON(http.StatusOK, certModel)
+ c.JSON(http.StatusOK, certModel)
}
func RemoveDomainFromAutoCert(c *gin.Context) {
- domain := c.Param("domain")
- domain = strings.ReplaceAll(domain, " ", "_")
- certModel := model.Cert{
- Domain: domain,
- }
-
- err := certModel.Updates(&model.Cert{
- AutoCert: model.AutoCertDisabled,
- })
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
- c.JSON(http.StatusOK, nil)
+ name := c.Param("name")
+ certModel := model.Cert{
+ Filename: name,
+ }
+
+ err := certModel.Updates(&model.Cert{
+ AutoCert: model.AutoCertDisabled,
+ })
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+ c.JSON(http.StatusOK, nil)
}
func DuplicateSite(c *gin.Context) {
- name := c.Param("name")
+ name := c.Param("name")
- var json struct {
- Name string `json:"name" binding:"required"`
- }
+ var json struct {
+ Name string `json:"name" binding:"required"`
+ }
- if !BindAndValid(c, &json) {
- return
- }
+ if !BindAndValid(c, &json) {
+ return
+ }
- src := nginx.GetConfPath("sites-available", name)
- dst := nginx.GetConfPath("sites-available", json.Name)
+ src := nginx.GetConfPath("sites-available", name)
+ dst := nginx.GetConfPath("sites-available", json.Name)
- if helper.FileExists(dst) {
- c.JSON(http.StatusNotAcceptable, gin.H{
- "message": "File exists",
- })
- return
- }
+ if helper.FileExists(dst) {
+ c.JSON(http.StatusNotAcceptable, gin.H{
+ "message": "File exists",
+ })
+ return
+ }
- _, err := helper.CopyFile(src, dst)
+ _, err := helper.CopyFile(src, dst)
- if err != nil {
- ErrHandler(c, err)
- return
- }
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
- c.JSON(http.StatusOK, gin.H{
- "dst": dst,
- })
+ c.JSON(http.StatusOK, gin.H{
+ "dst": dst,
+ })
}
diff --git a/server/model/cert.go b/server/model/cert.go
index ece89584e..c3b1ef0d6 100644
--- a/server/model/cert.go
+++ b/server/model/cert.go
@@ -2,6 +2,7 @@ package model
import (
"github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+ "github.com/lib/pq"
"os"
)
@@ -10,28 +11,35 @@ const (
AutoCertDisabled = -1
)
+type CertDomains []string
+
type Cert struct {
Model
- 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"`
+ Name string `json:"name"`
+ Domains pq.StringArray `json:"domains" gorm:"type:text[]"`
+ Filename string `json:"filename"`
+ 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(confName string) (c Cert, err error) {
err = db.First(&c, &Cert{
- Domain: domain,
+ Filename: confName,
}).Error
return
}
-func FirstOrCreateCert(domain string) (c Cert, err error) {
- err = db.FirstOrCreate(&c, &Cert{Domain: domain}).Error
+func FirstOrCreateCert(confName string) (c Cert, err error) {
+ err = db.FirstOrCreate(&c, &Cert{Filename: confName}).Error
return
}
+func (c *Cert) Insert() error {
+ return db.Create(c).Error
+}
+
func GetAutoCertList() (c []Cert) {
var t []Cert
db.Where("auto_cert", AutoCertEnabled).Find(&t)
@@ -49,7 +57,7 @@ func GetAutoCertList() (c []Cert) {
}
for _, v := range t {
- if enabledConfigMap[v.Domain] == true {
+ if enabledConfigMap[v.Filename] == true {
c = append(c, v)
}
}
@@ -76,9 +84,9 @@ func FirstCertByID(id int) (c Cert, err error) {
}
func (c *Cert) Updates(n *Cert) error {
- return db.Model(c).Updates(n).Error
+ return db.Model(&Cert{}).Where("filename", c.Filename).Updates(n).Error
}
func (c *Cert) Remove() error {
- return db.Where("domain", c.Domain).Delete(c).Error
+ return db.Where("filename", c.Filename).Delete(c).Error
}
diff --git a/server/pkg/cert/auto_cert.go b/server/pkg/cert/auto_cert.go
index 9d5e08044..2a9915587 100644
--- a/server/pkg/cert/auto_cert.go
+++ b/server/pkg/cert/auto_cert.go
@@ -3,7 +3,6 @@ package cert
import (
"github.com/0xJacky/Nginx-UI/server/model"
"log"
- "strings"
"time"
)
@@ -19,7 +18,7 @@ func handleIssueCertLogChan(logChan chan string) {
}
}
-func AutoCert() {
+func AutoObtain() {
defer func() {
if err := recover(); err != nil {
log.Println("[AutoCert] Recover", err)
@@ -27,19 +26,12 @@ func AutoCert() {
}()
log.Println("[AutoCert] Start")
autoCertList := model.GetAutoCertList()
- for i := range autoCertList {
- domain := autoCertList[i].Domain
-
- certModel, err := model.FirstCert(domain)
-
- if err != nil {
- log.Println("[AutoCert] Error get certificate from database", err)
- continue
- }
+ for _, certModel := range autoCertList {
+ confName := certModel.Filename
if certModel.SSLCertificatePath == "" {
log.Println("[AutoCert] Error ssl_certificate_path is empty, " +
- "try to reopen auto-cert for this domain:" + domain)
+ "try to reopen auto-cert for this config:" + confName)
continue
}
@@ -49,16 +41,17 @@ func AutoCert() {
// Get certificate info error, ignore this domain
continue
}
- // before 1 mo
- if time.Now().Before(cert.NotBefore.AddDate(0, 1, 0)) {
+ // every week
+ if time.Now().Sub(cert.NotBefore).Hours()/24 < 7 {
continue
}
+ //
// after 1 mo, reissue certificate
logChan := make(chan string, 1)
errChan := make(chan error, 1)
// support SAN certification
- go IssueCert(strings.Split(domain, "_"), logChan, errChan)
+ go IssueCert(certModel.Domains, logChan, errChan)
go handleIssueCertLogChan(logChan)
@@ -69,4 +62,5 @@ func AutoCert() {
close(logChan)
}
+ log.Println("[AutoCert] End")
}
diff --git a/server/pkg/cert/cert.go b/server/pkg/cert/cert.go
index 7979c52df..ff7c9f50a 100644
--- a/server/pkg/cert/cert.go
+++ b/server/pkg/cert/cert.go
@@ -1,148 +1,155 @@
package cert
import (
- "crypto"
- "crypto/ecdsa"
- "crypto/elliptic"
- "crypto/rand"
- "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
- "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"
- "github.com/pkg/errors"
- "log"
- "os"
- "path/filepath"
- "strings"
+ "crypto"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/tls"
+ "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+ "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"
+ "github.com/pkg/errors"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
)
// MyUser You'll need a user or account type that implements acme.User
type MyUser struct {
- Email string
- Registration *registration.Resource
- key crypto.PrivateKey
+ Email string
+ Registration *registration.Resource
+ key crypto.PrivateKey
}
func (u *MyUser) GetEmail() string {
- return u.Email
+ return u.Email
}
func (u *MyUser) GetRegistration() *registration.Resource {
- return u.Registration
+ return u.Registration
}
func (u *MyUser) GetPrivateKey() crypto.PrivateKey {
- return u.key
+ return u.key
}
func IssueCert(domain []string, logChan chan string, errChan chan error) {
- defer func() {
- if err := recover(); err != nil {
- log.Println("Issue Cert recover", err)
- }
- }()
-
- // Create a user. New accounts need an email and private key to start.
- logChan <- "Generating private key for registering account"
- privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
- if err != nil {
- errChan <- errors.Wrap(err, "issue cert generate key error")
- return
- }
-
- logChan <- "Preparing lego configurations"
- 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"
- }
-
- if settings.ServerSettings.CADir != "" {
- config.CADirURL = settings.ServerSettings.CADir
- }
-
- config.Certificate.KeyType = certcrypto.RSA2048
-
- logChan <- "Creating client facilitates communication with the CA server"
- // A client facilitates communication with the CA server.
- client, err := lego.NewClient(config)
- if err != nil {
- errChan <- errors.Wrap(err, "issue cert new client error")
- return
- }
-
- logChan <- "Using HTTP01 challenge provider"
- err = client.Challenge.SetHTTP01Provider(
- http01.NewProviderServer("",
- settings.ServerSettings.HTTPChallengePort,
- ),
- )
-
- if err != nil {
- errChan <- errors.Wrap(err, "issue cert challenge fail")
- return
- }
-
- // New users will need to register
- logChan <- "Registering user"
- reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
- if err != nil {
- errChan <- errors.Wrap(err, "issue cert register fail")
- return
- }
- myUser.Registration = reg
-
- request := certificate.ObtainRequest{
- Domains: domain,
- Bundle: true,
- }
-
- logChan <- "Obtaining certificate"
- certificates, err := client.Certificate.Obtain(request)
- if err != nil {
- errChan <- errors.Wrap(err, "issue cert fail to obtain")
- return
- }
- name := strings.Join(domain, "_")
- saveDir := nginx.GetConfPath("ssl/" + name)
- if _, err = os.Stat(saveDir); os.IsNotExist(err) {
- err = os.MkdirAll(saveDir, 0755)
- if err != nil {
- errChan <- errors.Wrap(err, "issue cert fail to create")
- return
- }
- }
-
- // Each certificate comes back with the cert bytes, the bytes of the client's
- // private key, and a certificate URL. SAVE THESE TO DISK.
- logChan <- "Writing certificate to disk"
- err = os.WriteFile(filepath.Join(saveDir, "fullchain.cer"),
- certificates.Certificate, 0644)
-
- if err != nil {
- errChan <- errors.Wrap(err, "error issue cert write fullchain.cer")
- return
- }
-
- logChan <- "Writing certificate private key to disk"
- err = os.WriteFile(filepath.Join(saveDir, "private.key"),
- certificates.PrivateKey, 0644)
-
- if err != nil {
- errChan <- errors.Wrap(err, "error issue cert write key")
- return
- }
-
- close(errChan)
- logChan <- "Reloading nginx"
-
- nginx.Reload()
-
- logChan <- "Finished"
+ defer func() {
+ if err := recover(); err != nil {
+ log.Println("Issue Cert recover", err)
+ }
+ }()
+
+ // Create a user. New accounts need an email and private key to start.
+ logChan <- "Generating private key for registering account"
+ privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ errChan <- errors.Wrap(err, "issue cert generate key error")
+ return
+ }
+
+ logChan <- "Preparing lego configurations"
+ 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"
+ }
+
+ if settings.ServerSettings.CADir != "" {
+ config.CADirURL = settings.ServerSettings.CADir
+ if config.HTTPClient != nil {
+ config.HTTPClient.Transport = &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ }
+ }
+ }
+
+ config.Certificate.KeyType = certcrypto.RSA2048
+
+ logChan <- "Creating client facilitates communication with the CA server"
+ // A client facilitates communication with the CA server.
+ client, err := lego.NewClient(config)
+ if err != nil {
+ errChan <- errors.Wrap(err, "issue cert new client error")
+ return
+ }
+
+ logChan <- "Using HTTP01 challenge provider"
+ err = client.Challenge.SetHTTP01Provider(
+ http01.NewProviderServer("",
+ settings.ServerSettings.HTTPChallengePort,
+ ),
+ )
+
+ if err != nil {
+ errChan <- errors.Wrap(err, "issue cert challenge fail")
+ return
+ }
+
+ // New users will need to register
+ logChan <- "Registering user"
+ reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
+ if err != nil {
+ errChan <- errors.Wrap(err, "issue cert register fail")
+ return
+ }
+ myUser.Registration = reg
+
+ request := certificate.ObtainRequest{
+ Domains: domain,
+ Bundle: true,
+ }
+
+ logChan <- "Obtaining certificate"
+ certificates, err := client.Certificate.Obtain(request)
+ if err != nil {
+ errChan <- errors.Wrap(err, "issue cert fail to obtain")
+ return
+ }
+ name := strings.Join(domain, "_")
+ saveDir := nginx.GetConfPath("ssl/" + name)
+ if _, err = os.Stat(saveDir); os.IsNotExist(err) {
+ err = os.MkdirAll(saveDir, 0755)
+ if err != nil {
+ errChan <- errors.Wrap(err, "issue cert fail to create")
+ return
+ }
+ }
+
+ // Each certificate comes back with the cert bytes, the bytes of the client's
+ // private key, and a certificate URL. SAVE THESE TO DISK.
+ logChan <- "Writing certificate to disk"
+ err = os.WriteFile(filepath.Join(saveDir, "fullchain.cer"),
+ certificates.Certificate, 0644)
+
+ if err != nil {
+ errChan <- errors.Wrap(err, "error issue cert write fullchain.cer")
+ return
+ }
+
+ logChan <- "Writing certificate private key to disk"
+ err = os.WriteFile(filepath.Join(saveDir, "private.key"),
+ certificates.PrivateKey, 0644)
+
+ if err != nil {
+ errChan <- errors.Wrap(err, "error issue cert write key")
+ return
+ }
+
+ close(errChan)
+ logChan <- "Reloading nginx"
+
+ nginx.Reload()
+
+ logChan <- "Finished"
}
diff --git a/server/pkg/nginx/type.go b/server/pkg/nginx/type.go
index cf803e067..c436c3502 100644
--- a/server/pkg/nginx/type.go
+++ b/server/pkg/nginx/type.go
@@ -1,61 +1,64 @@
package nginx
import (
- "github.com/tufanbarisyildirim/gonginx"
- "strings"
+ "github.com/tufanbarisyildirim/gonginx"
+ "path"
+ "strings"
)
type NgxConfig struct {
- FileName string `json:"file_name"`
- Upstreams []*NgxUpstream `json:"upstreams"`
- Servers []*NgxServer `json:"servers"`
- Custom string `json:"custom"`
- c *gonginx.Config
+ FileName string `json:"file_name"`
+ Name string `json:"name"`
+ Upstreams []*NgxUpstream `json:"upstreams"`
+ Servers []*NgxServer `json:"servers"`
+ Custom string `json:"custom"`
+ c *gonginx.Config
}
type NgxServer struct {
- Directives []*NgxDirective `json:"directives"`
- Locations []*NgxLocation `json:"locations"`
- Comments string `json:"comments"`
+ Directives []*NgxDirective `json:"directives"`
+ Locations []*NgxLocation `json:"locations"`
+ Comments string `json:"comments"`
}
type NgxUpstream struct {
- Name string `json:"name"`
- Directives []*NgxDirective `json:"directives"`
- Comments string `json:"comments"`
+ Name string `json:"name"`
+ Directives []*NgxDirective `json:"directives"`
+ Comments string `json:"comments"`
}
type NgxDirective struct {
- Directive string `json:"directive"`
- Params string `json:"params"`
- Comments string `json:"comments"`
+ Directive string `json:"directive"`
+ Params string `json:"params"`
+ Comments string `json:"comments"`
}
type NgxLocation struct {
- Path string `json:"path"`
- Content string `json:"content"`
- Comments string `json:"comments"`
+ Path string `json:"path"`
+ Content string `json:"content"`
+ Comments string `json:"comments"`
}
func (d *NgxDirective) Orig() string {
- return d.Directive + " " + d.Params
+ return d.Directive + " " + d.Params
}
func (d *NgxDirective) TrimParams() {
- d.Params = strings.TrimRight(strings.TrimSpace(d.Params), ";")
- return
+ d.Params = strings.TrimRight(strings.TrimSpace(d.Params), ";")
+ return
}
func NewNgxServer() *NgxServer {
- return &NgxServer{
- Locations: make([]*NgxLocation, 0),
- Directives: make([]*NgxDirective, 0),
- }
+ return &NgxServer{
+ Locations: make([]*NgxLocation, 0),
+ Directives: make([]*NgxDirective, 0),
+ }
}
func NewNgxConfig(filename string) *NgxConfig {
- return &NgxConfig{
- FileName: filename,
- Upstreams: make([]*NgxUpstream, 0),
- }
+ return &NgxConfig{
+ FileName: filename,
+ Upstreams: make([]*NgxUpstream, 0),
+ Name: path.Base(filename),
+ }
}
diff --git a/server/router/routers.go b/server/router/routers.go
index 834b2afc2..51a988457 100644
--- a/server/router/routers.go
+++ b/server/router/routers.go
@@ -76,6 +76,7 @@ func InitRouter() *gin.Engine {
g.DELETE("domain/:name", api.DeleteDomain)
// duplicate site
g.POST("domain/:name/duplicate", api.DuplicateSite)
+ g.GET("domain/:name/cert", api.IssueCert)
g.GET("configs", api.GetConfigs)
g.GET("config/*name", api.GetConfig)
@@ -90,17 +91,15 @@ func InitRouter() *gin.Engine {
g.GET("template/blocks", api.GetTemplateBlockList)
g.GET("template/block/:name", api.GetTemplateBlock)
- g.GET("cert/issue", api.IssueCert)
-
g.GET("certs", api.GetCertList)
g.GET("cert/:id", api.GetCert)
g.POST("cert", api.AddCert)
g.POST("cert/:id", api.ModifyCert)
g.DELETE("cert/:id", api.RemoveCert)
// Add domain to auto-renew cert list
- g.POST("auto_cert/:domain", api.AddDomainToAutoCert)
+ g.POST("auto_cert/:name", api.AddDomainToAutoCert)
// Delete domain from auto-renew cert list
- g.DELETE("auto_cert/:domain", api.RemoveDomainFromAutoCert)
+ g.DELETE("auto_cert/:name", api.RemoveDomainFromAutoCert)
// pty
g.GET("pty", api.Pty)
From e260860adc9d52439c23d7908434324240f5266f Mon Sep 17 00:00:00 2001
From: 0xJacky
Date: Wed, 15 Feb 2023 11:11:16 +0800
Subject: [PATCH 2/6] feat: store error log of obtaining cert
---
server/api/domain.go | 9 +++--
server/model/cert.go | 7 ++--
server/pkg/cert/auto_cert.go | 64 +++++++++++++++++++++++++++++++++---
3 files changed, 70 insertions(+), 10 deletions(-)
diff --git a/server/api/domain.go b/server/api/domain.go
index 8f705e14c..796c953ec 100644
--- a/server/api/domain.go
+++ b/server/api/domain.go
@@ -377,11 +377,14 @@ func AddDomainToAutoCert(c *gin.Context) {
func RemoveDomainFromAutoCert(c *gin.Context) {
name := c.Param("name")
- certModel := model.Cert{
- Filename: name,
+ certModel, err := model.FirstCert(name)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
}
- err := certModel.Updates(&model.Cert{
+ err = certModel.Updates(&model.Cert{
AutoCert: model.AutoCertDisabled,
})
diff --git a/server/model/cert.go b/server/model/cert.go
index c3b1ef0d6..7bc28da44 100644
--- a/server/model/cert.go
+++ b/server/model/cert.go
@@ -21,6 +21,7 @@ type Cert struct {
SSLCertificatePath string `json:"ssl_certificate_path"`
SSLCertificateKeyPath string `json:"ssl_certificate_key_path"`
AutoCert int `json:"auto_cert"`
+ Log string `json:"log"`
}
func FirstCert(confName string) (c Cert, err error) {
@@ -40,8 +41,8 @@ func (c *Cert) Insert() error {
return db.Create(c).Error
}
-func GetAutoCertList() (c []Cert) {
- var t []Cert
+func GetAutoCertList() (c []*Cert) {
+ var t []*Cert
db.Where("auto_cert", AutoCertEnabled).Find(&t)
// check if this domain is enabled
@@ -84,7 +85,7 @@ func FirstCertByID(id int) (c Cert, err error) {
}
func (c *Cert) Updates(n *Cert) error {
- return db.Model(&Cert{}).Where("filename", c.Filename).Updates(n).Error
+ return db.Model(&Cert{}).Where("id", c.ID).Updates(n).Error
}
func (c *Cert) Remove() error {
diff --git a/server/pkg/cert/auto_cert.go b/server/pkg/cert/auto_cert.go
index 2a9915587..410002200 100644
--- a/server/pkg/cert/auto_cert.go
+++ b/server/pkg/cert/auto_cert.go
@@ -1,7 +1,9 @@
package cert
import (
+ "fmt"
"github.com/0xJacky/Nginx-UI/server/model"
+ "github.com/pkg/errors"
"log"
"time"
)
@@ -18,6 +20,42 @@ func handleIssueCertLogChan(logChan chan string) {
}
}
+type AutoCertErrorLog struct {
+ buffer []string
+ cert *model.Cert
+}
+
+func (t *AutoCertErrorLog) SetCertModel(cert *model.Cert) {
+ t.cert = cert
+}
+
+func (t *AutoCertErrorLog) Push(text string, err error) {
+ t.buffer = append(t.buffer, text+" "+err.Error())
+ log.Println("[AutoCert Error]", text, err)
+}
+
+func (t *AutoCertErrorLog) Exit(text string, err error) {
+ t.buffer = append(t.buffer, text+" "+err.Error())
+ log.Println("[AutoCert Error]", text, err)
+
+ if t.cert == nil {
+ return
+ }
+
+ _ = t.cert.Updates(&model.Cert{
+ Log: t.ToString(),
+ })
+}
+
+func (t *AutoCertErrorLog) ToString() (content string) {
+
+ for _, v := range t.buffer {
+ content += fmt.Sprintf("[AutoCert Error] %s\n", v)
+ }
+
+ return
+}
+
func AutoObtain() {
defer func() {
if err := recover(); err != nil {
@@ -29,15 +67,29 @@ func AutoObtain() {
for _, certModel := range autoCertList {
confName := certModel.Filename
+ errLog := &AutoCertErrorLog{}
+ errLog.SetCertModel(certModel)
+
+ if len(certModel.Filename) == 0 {
+ errLog.Exit("", errors.New("filename is empty"))
+ continue
+ }
+
+ if len(certModel.Domains) == 0 {
+ errLog.Exit(confName, errors.New("domains list is empty, "+
+ "try to reopen auto-cert for this config:"+confName))
+ continue
+ }
+
if certModel.SSLCertificatePath == "" {
- log.Println("[AutoCert] Error ssl_certificate_path is empty, " +
- "try to reopen auto-cert for this config:" + confName)
+ errLog.Exit(confName, errors.New("ssl_certificate_path is empty, "+
+ "try to reopen auto-cert for this config:"+confName))
continue
}
cert, err := GetCertInfo(certModel.SSLCertificatePath)
if err != nil {
- log.Println("GetCertInfo Err", err)
+ errLog.Push("get cert info", err)
// Get certificate info error, ignore this domain
continue
}
@@ -57,8 +109,12 @@ func AutoObtain() {
// block, unless errChan closed
for err = range errChan {
- log.Println("Error cert.IssueCert", err)
+ errLog.Push("issue cert", err)
}
+ // store error log to db
+ _ = certModel.Updates(&model.Cert{
+ Log: errLog.ToString(),
+ })
close(logChan)
}
From e9d26ded1cf9551bae02b5556763199df5248736 Mon Sep 17 00:00:00 2001
From: 0xJacky
Date: Wed, 15 Feb 2023 11:20:43 +0800
Subject: [PATCH 3/6] enhance: auto obtain cert
---
frontend/src/views/cert/Cert.vue | 10 +--
server/api/domain.go | 1 +
server/model/cert.go | 104 ++++++++++++++++---------------
server/pkg/cert/auto_cert.go | 29 ++++-----
4 files changed, 70 insertions(+), 74 deletions(-)
diff --git a/frontend/src/views/cert/Cert.vue b/frontend/src/views/cert/Cert.vue
index edfe8afef..b160034aa 100644
--- a/frontend/src/views/cert/Cert.vue
+++ b/frontend/src/views/cert/Cert.vue
@@ -29,14 +29,10 @@ const columns = [{
},
search: true
}, {
- title: () => $gettext('Domain'),
- dataIndex: 'domain',
+ title: () => $gettext('Config Name'),
+ dataIndex: 'filename',
sorter: true,
- pithy: true,
- edit: {
- type: input
- },
- search: true
+ pithy: true
}, {
title: () => $gettext('Auto Cert'),
dataIndex: 'auto_cert',
diff --git a/server/api/domain.go b/server/api/domain.go
index 796c953ec..2f7d0c995 100644
--- a/server/api/domain.go
+++ b/server/api/domain.go
@@ -364,6 +364,7 @@ func AddDomainToAutoCert(c *gin.Context) {
}
err = certModel.Updates(&model.Cert{
+ Name: name,
AutoCert: model.AutoCertEnabled,
})
diff --git a/server/model/cert.go b/server/model/cert.go
index 7bc28da44..f5186da64 100644
--- a/server/model/cert.go
+++ b/server/model/cert.go
@@ -1,93 +1,97 @@
package model
import (
- "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
- "github.com/lib/pq"
- "os"
+ "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+ "github.com/lib/pq"
+ "os"
)
const (
- AutoCertEnabled = 1
- AutoCertDisabled = -1
+ AutoCertEnabled = 1
+ AutoCertDisabled = -1
)
type CertDomains []string
type Cert struct {
- Model
- Name string `json:"name"`
- Domains pq.StringArray `json:"domains" gorm:"type:text[]"`
- Filename string `json:"filename"`
- SSLCertificatePath string `json:"ssl_certificate_path"`
- SSLCertificateKeyPath string `json:"ssl_certificate_key_path"`
- AutoCert int `json:"auto_cert"`
- Log string `json:"log"`
+ Model
+ Name string `json:"name"`
+ Domains pq.StringArray `json:"domains" gorm:"type:text[]"`
+ Filename string `json:"filename"`
+ SSLCertificatePath string `json:"ssl_certificate_path"`
+ SSLCertificateKeyPath string `json:"ssl_certificate_key_path"`
+ AutoCert int `json:"auto_cert"`
+ Log string `json:"log"`
}
func FirstCert(confName string) (c Cert, err error) {
- err = db.First(&c, &Cert{
- Filename: confName,
- }).Error
+ err = db.First(&c, &Cert{
+ Filename: confName,
+ }).Error
- return
+ return
}
func FirstOrCreateCert(confName string) (c Cert, err error) {
- err = db.FirstOrCreate(&c, &Cert{Filename: confName}).Error
- return
+ err = db.FirstOrCreate(&c, &Cert{Filename: confName}).Error
+ return
}
func (c *Cert) Insert() error {
- return db.Create(c).Error
+ return db.Create(c).Error
}
func GetAutoCertList() (c []*Cert) {
- var t []*Cert
- db.Where("auto_cert", AutoCertEnabled).Find(&t)
+ var t []*Cert
+ db.Where("auto_cert", AutoCertEnabled).Find(&t)
- // check if this domain is enabled
- enabledConfig, err := os.ReadDir(nginx.GetConfPath("sites-enabled"))
+ // check if this domain is enabled
+ enabledConfig, err := os.ReadDir(nginx.GetConfPath("sites-enabled"))
- if err != nil {
- return
- }
+ if err != nil {
+ return
+ }
- enabledConfigMap := make(map[string]bool)
- for i := range enabledConfig {
- enabledConfigMap[enabledConfig[i].Name()] = true
- }
+ enabledConfigMap := make(map[string]bool)
+ for i := range enabledConfig {
+ enabledConfigMap[enabledConfig[i].Name()] = true
+ }
- for _, v := range t {
- if enabledConfigMap[v.Filename] == true {
- c = append(c, v)
- }
- }
+ for _, v := range t {
+ if enabledConfigMap[v.Filename] == true {
+ c = append(c, v)
+ }
+ }
- 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
+ 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
+ err = db.First(&c, id).Error
- return
+ return
}
func (c *Cert) Updates(n *Cert) error {
- return db.Model(&Cert{}).Where("id", c.ID).Updates(n).Error
+ return db.Model(&Cert{}).Where("id", c.ID).Updates(n).Error
}
func (c *Cert) Remove() error {
- return db.Where("filename", c.Filename).Delete(c).Error
+ if c.Filename == "" {
+ return db.Delete(c).Error
+ }
+
+ return db.Where("filename", c.Filename).Delete(c).Error
}
diff --git a/server/pkg/cert/auto_cert.go b/server/pkg/cert/auto_cert.go
index 410002200..ef26d106e 100644
--- a/server/pkg/cert/auto_cert.go
+++ b/server/pkg/cert/auto_cert.go
@@ -81,23 +81,18 @@ func AutoObtain() {
continue
}
- if certModel.SSLCertificatePath == "" {
- errLog.Exit(confName, errors.New("ssl_certificate_path is empty, "+
- "try to reopen auto-cert for this config:"+confName))
- continue
- }
-
- cert, err := GetCertInfo(certModel.SSLCertificatePath)
- if err != nil {
- errLog.Push("get cert info", err)
- // Get certificate info error, ignore this domain
- continue
- }
- // every week
- if time.Now().Sub(cert.NotBefore).Hours()/24 < 7 {
- continue
+ if certModel.SSLCertificatePath != "" {
+ cert, err := GetCertInfo(certModel.SSLCertificatePath)
+ if err != nil {
+ errLog.Push("get cert info", err)
+ // Get certificate info error, ignore this domain
+ continue
+ }
+ // every week
+ if time.Now().Sub(cert.NotBefore).Hours()/24 < 7 {
+ continue
+ }
}
- //
// after 1 mo, reissue certificate
logChan := make(chan string, 1)
errChan := make(chan error, 1)
@@ -108,7 +103,7 @@ func AutoObtain() {
go handleIssueCertLogChan(logChan)
// block, unless errChan closed
- for err = range errChan {
+ for err := range errChan {
errLog.Push("issue cert", err)
}
// store error log to db
From a9aacce0ad5b37ed33edb97897e7224cd372eb4d Mon Sep 17 00:00:00 2001
From: 0xJacky
Date: Wed, 15 Feb 2023 12:51:12 +0800
Subject: [PATCH 4/6] feat: store auto-cert log to db
---
frontend/src/api/domain.ts | 4 +-
frontend/src/views/cert/Cert.vue | 36 +-
frontend/src/views/domain/cert/IssueCert.vue | 2 +-
server/api/cert.go | 607 +++++++--------
server/api/domain.go | 746 ++++++++++---------
server/model/cert.go | 110 +--
server/pkg/cert/auto_cert.go | 182 ++---
server/pkg/cert/cert.go | 12 +-
8 files changed, 872 insertions(+), 827 deletions(-)
diff --git a/frontend/src/api/domain.ts b/frontend/src/api/domain.ts
index fceab1793..201ea8b9a 100644
--- a/frontend/src/api/domain.ts
+++ b/frontend/src/api/domain.ts
@@ -14,8 +14,8 @@ class Domain extends Curd {
return http.get('template')
}
- add_auto_cert(domain: string) {
- return http.post('auto_cert/' + domain)
+ add_auto_cert(domain: string, data: any) {
+ return http.post('auto_cert/' + domain, data)
}
remove_auto_cert(domain: string) {
diff --git a/frontend/src/views/cert/Cert.vue b/frontend/src/views/cert/Cert.vue
index b160034aa..e949f5981 100644
--- a/frontend/src/views/cert/Cert.vue
+++ b/frontend/src/views/cert/Cert.vue
@@ -2,15 +2,15 @@
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'
+import {h} from 'vue'
-const {$gettext} = useGettext()
+const {$gettext, interpolate} = useGettext()
const columns = [{
title: () => $gettext('Name'),
@@ -81,11 +81,33 @@ const columns = [{
row-key="name"
>
-
+
+
+
+
+
+
+
+ {{ data.log }}
+
+
+
+
diff --git a/frontend/src/views/domain/cert/IssueCert.vue b/frontend/src/views/domain/cert/IssueCert.vue
index 274e9c53b..5771fa041 100644
--- a/frontend/src/views/domain/cert/IssueCert.vue
+++ b/frontend/src/views/domain/cert/IssueCert.vue
@@ -61,7 +61,7 @@ function callback(ssl_certificate: string, ssl_certificate_key: string) {
function change_auto_cert(r: boolean) {
if (r) {
- domain.add_auto_cert(props.config_name).then(() => {
+ domain.add_auto_cert(props.config_name, {domains: name.value.trim().split(' ')}).then(() => {
message.success(interpolate($gettext('Auto-renewal enabled for %{name}'), {name: name.value}))
}).catch(e => {
message.error(e.message ?? interpolate($gettext('Enable auto-renewal failed for %{name}'), {name: name.value}))
diff --git a/server/api/cert.go b/server/api/cert.go
index bee6ae491..230f96190 100644
--- a/server/api/cert.go
+++ b/server/api/cert.go
@@ -1,349 +1,350 @@
package api
import (
- "github.com/0xJacky/Nginx-UI/server/model"
- "github.com/0xJacky/Nginx-UI/server/pkg/cert"
- "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
- "github.com/gin-gonic/gin"
- "github.com/gorilla/websocket"
- "github.com/spf13/cast"
- "log"
- "net/http"
- "os"
- "path/filepath"
- "strings"
+ "github.com/0xJacky/Nginx-UI/server/model"
+ "github.com/0xJacky/Nginx-UI/server/pkg/cert"
+ "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+ "github.com/gin-gonic/gin"
+ "github.com/gorilla/websocket"
+ "github.com/spf13/cast"
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
)
const (
- Success = "success"
- Info = "info"
- Error = "error"
+ Success = "success"
+ Info = "info"
+ Error = "error"
)
type IssueCertResponse struct {
- Status string `json:"status"`
- Message string `json:"message"`
- SSLCertificate string `json:"ssl_certificate,omitempty"`
- SSLCertificateKey string `json:"ssl_certificate_key,omitempty"`
+ Status string `json:"status"`
+ Message string `json:"message"`
+ SSLCertificate string `json:"ssl_certificate,omitempty"`
+ SSLCertificateKey string `json:"ssl_certificate_key,omitempty"`
}
func handleIssueCertLogChan(conn *websocket.Conn, logChan chan string) {
- defer func() {
- if err := recover(); err != nil {
- log.Println("api.handleIssueCertLogChan recover", err)
- }
- }()
+ defer func() {
+ if err := recover(); err != nil {
+ log.Println("api.handleIssueCertLogChan recover", err)
+ }
+ }()
- for logString := range logChan {
+ for logString := range logChan {
- err := conn.WriteJSON(IssueCertResponse{
- Status: Info,
- Message: logString,
- })
+ err := conn.WriteJSON(IssueCertResponse{
+ Status: Info,
+ Message: logString,
+ })
- if err != nil {
- log.Println("Error handleIssueCertLogChan", err)
- return
- }
+ if err != nil {
+ log.Println("Error handleIssueCertLogChan", err)
+ return
+ }
- }
+ }
}
func IssueCert(c *gin.Context) {
- 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("defer websocket close err", err)
- }
- }(ws)
-
- // read
- var buffer struct {
- ServerName []string `json:"server_name"`
- }
-
- err = ws.ReadJSON(&buffer)
-
- if err != nil {
- log.Println(err)
- return
- }
-
- logChan := make(chan string, 1)
- errChan := make(chan error, 1)
-
- go cert.IssueCert(buffer.ServerName, logChan, errChan)
-
- go handleIssueCertLogChan(ws, logChan)
-
- // block, unless errChan closed
- for err = range errChan {
- log.Println("Error cert.IssueCert", err)
-
- err = ws.WriteJSON(IssueCertResponse{
- Status: Error,
- Message: err.Error(),
- })
-
- if err != nil {
- log.Println("Error WriteJSON", err)
- return
- }
-
- return
- }
-
- close(logChan)
-
- certDirName := strings.Join(buffer.ServerName, "_")
- sslCertificatePath := nginx.GetConfPath("ssl", certDirName, "fullchain.cer")
- sslCertificateKeyPath := nginx.GetConfPath("ssl", certDirName, "private.key")
-
- certModel, err := model.FirstOrCreateCert(c.Param("name"))
-
- if err != nil {
- log.Println(err)
- }
-
- err = certModel.Updates(&model.Cert{
- Domains: buffer.ServerName,
- SSLCertificatePath: sslCertificatePath,
- SSLCertificateKeyPath: sslCertificateKeyPath,
- })
-
- if err != nil {
- log.Println(err)
- err = ws.WriteJSON(IssueCertResponse{
- Status: Error,
- Message: err.Error(),
- })
- return
- }
-
- err = ws.WriteJSON(IssueCertResponse{
- Status: Success,
- Message: "Issued certificate successfully",
- SSLCertificate: sslCertificatePath,
- SSLCertificateKey: sslCertificateKeyPath,
- })
-
- if err != nil {
- log.Println(err)
- return
- }
+ 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("defer websocket close err", err)
+ }
+ }(ws)
+
+ // read
+ var buffer struct {
+ ServerName []string `json:"server_name"`
+ }
+
+ err = ws.ReadJSON(&buffer)
+
+ if err != nil {
+ log.Println(err)
+ return
+ }
+
+ certModel, err := model.FirstOrCreateCert(c.Param("name"))
+
+ if err != nil {
+ log.Println(err)
+ }
+
+ logChan := make(chan string, 1)
+ errChan := make(chan error, 1)
+
+ go cert.IssueCert(buffer.ServerName, logChan, errChan)
+
+ go handleIssueCertLogChan(ws, logChan)
+
+ // block, until errChan closes
+ for err = range errChan {
+ errLog := &cert.AutoCertErrorLog{}
+ errLog.SetCertModel(&certModel)
+ errLog.Exit("issue cert", err)
+
+ err = ws.WriteJSON(IssueCertResponse{
+ Status: Error,
+ Message: err.Error(),
+ })
+
+ if err != nil {
+ log.Println("Error WriteJSON", err)
+ return
+ }
+
+ return
+ }
+
+ certDirName := strings.Join(buffer.ServerName, "_")
+ sslCertificatePath := nginx.GetConfPath("ssl", certDirName, "fullchain.cer")
+ sslCertificateKeyPath := nginx.GetConfPath("ssl", certDirName, "private.key")
+
+ err = certModel.Updates(&model.Cert{
+ Domains: buffer.ServerName,
+ SSLCertificatePath: sslCertificatePath,
+ SSLCertificateKeyPath: sslCertificateKeyPath,
+ })
+
+ if err != nil {
+ log.Println(err)
+ err = ws.WriteJSON(IssueCertResponse{
+ Status: Error,
+ Message: err.Error(),
+ })
+ return
+ }
+
+ certModel.ClearLog()
+
+ err = ws.WriteJSON(IssueCertResponse{
+ Status: Success,
+ Message: "Issued certificate successfully",
+ SSLCertificate: sslCertificatePath,
+ SSLCertificateKey: sslCertificateKeyPath,
+ })
+
+ if err != nil {
+ log.Println(err)
+ return
+ }
}
func GetCertList(c *gin.Context) {
- certList := model.GetCertList(c.Query("name"), c.Query("domain"))
+ certList := model.GetCertList(c.Query("name"), c.Query("domain"))
- c.JSON(http.StatusOK, gin.H{
- "data": certList,
- })
+ 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,
- })
+ 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")))
+ certModel, err := model.FirstCertByID(cast.ToInt(c.Param("id")))
- if err != nil {
- ErrHandler(c, err)
- return
- }
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
- getCert(c, &certModel)
+ getCert(c, &certModel)
}
func AddCert(c *gin.Context) {
- var json struct {
- Name string `json:"name"`
- 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 := &model.Cert{
- Name: json.Name,
- SSLCertificatePath: json.SSLCertificatePath,
- SSLCertificateKeyPath: json.SSLCertificateKeyPath,
- }
-
- err := certModel.Insert()
-
- 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)
+ var json struct {
+ Name string `json:"name"`
+ 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 := &model.Cert{
+ Name: json.Name,
+ SSLCertificatePath: json.SSLCertificatePath,
+ SSLCertificateKeyPath: json.SSLCertificateKeyPath,
+ }
+
+ err := certModel.Insert()
+
+ 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,
- 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)
+ id := cast.ToInt(c.Param("id"))
+ certModel, err := model.FirstCertByID(id)
+
+ var json struct {
+ Name string `json:"name"`
+ 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,
+ 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)
+ id := cast.ToInt(c.Param("id"))
+ certModel, err := model.FirstCertByID(id)
- if err != nil {
- ErrHandler(c, err)
- return
- }
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
- err = certModel.Remove()
+ err = certModel.Remove()
- if err != nil {
- ErrHandler(c, err)
- return
- }
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
- c.JSON(http.StatusNoContent, nil)
+ c.JSON(http.StatusNoContent, nil)
}
diff --git a/server/api/domain.go b/server/api/domain.go
index 2f7d0c995..e5faf9f2a 100644
--- a/server/api/domain.go
+++ b/server/api/domain.go
@@ -1,430 +1,440 @@
package api
import (
- "github.com/0xJacky/Nginx-UI/server/model"
- "github.com/0xJacky/Nginx-UI/server/pkg/cert"
- "github.com/0xJacky/Nginx-UI/server/pkg/config_list"
- "github.com/0xJacky/Nginx-UI/server/pkg/helper"
- "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
- "github.com/gin-gonic/gin"
- "log"
- "net/http"
- "os"
- "strings"
- "time"
+ "github.com/0xJacky/Nginx-UI/server/model"
+ "github.com/0xJacky/Nginx-UI/server/pkg/cert"
+ "github.com/0xJacky/Nginx-UI/server/pkg/config_list"
+ "github.com/0xJacky/Nginx-UI/server/pkg/helper"
+ "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+ "github.com/gin-gonic/gin"
+ "log"
+ "net/http"
+ "os"
+ "strings"
+ "time"
)
func GetDomains(c *gin.Context) {
- name := c.Query("name")
- orderBy := c.Query("order_by")
- sort := c.DefaultQuery("sort", "desc")
-
- mySort := map[string]string{
- "enabled": "bool",
- "name": "string",
- "modify": "time",
- }
-
- configFiles, err := os.ReadDir(nginx.GetConfPath("sites-available"))
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- enabledConfig, err := os.ReadDir(nginx.GetConfPath("sites-enabled"))
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- enabledConfigMap := make(map[string]bool)
- for i := range enabledConfig {
- enabledConfigMap[enabledConfig[i].Name()] = true
- }
-
- var configs []gin.H
-
- for i := range configFiles {
- file := configFiles[i]
- fileInfo, _ := file.Info()
- if !file.IsDir() {
- if name != "" && !strings.Contains(file.Name(), name) {
- continue
- }
- configs = append(configs, gin.H{
- "name": file.Name(),
- "size": fileInfo.Size(),
- "modify": fileInfo.ModTime(),
- "enabled": enabledConfigMap[file.Name()],
- })
- }
- }
-
- configs = config_list.Sort(orderBy, sort, mySort[orderBy], configs)
-
- c.JSON(http.StatusOK, gin.H{
- "data": configs,
- })
+ name := c.Query("name")
+ orderBy := c.Query("order_by")
+ sort := c.DefaultQuery("sort", "desc")
+
+ mySort := map[string]string{
+ "enabled": "bool",
+ "name": "string",
+ "modify": "time",
+ }
+
+ configFiles, err := os.ReadDir(nginx.GetConfPath("sites-available"))
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ enabledConfig, err := os.ReadDir(nginx.GetConfPath("sites-enabled"))
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ enabledConfigMap := make(map[string]bool)
+ for i := range enabledConfig {
+ enabledConfigMap[enabledConfig[i].Name()] = true
+ }
+
+ var configs []gin.H
+
+ for i := range configFiles {
+ file := configFiles[i]
+ fileInfo, _ := file.Info()
+ if !file.IsDir() {
+ if name != "" && !strings.Contains(file.Name(), name) {
+ continue
+ }
+ configs = append(configs, gin.H{
+ "name": file.Name(),
+ "size": fileInfo.Size(),
+ "modify": fileInfo.ModTime(),
+ "enabled": enabledConfigMap[file.Name()],
+ })
+ }
+ }
+
+ configs = config_list.Sort(orderBy, sort, mySort[orderBy], configs)
+
+ c.JSON(http.StatusOK, gin.H{
+ "data": configs,
+ })
}
type CertificateInfo struct {
- SubjectName string `json:"subject_name"`
- IssuerName string `json:"issuer_name"`
- NotAfter time.Time `json:"not_after"`
- NotBefore time.Time `json:"not_before"`
+ SubjectName string `json:"subject_name"`
+ IssuerName string `json:"issuer_name"`
+ NotAfter time.Time `json:"not_after"`
+ NotBefore time.Time `json:"not_before"`
}
func GetDomain(c *gin.Context) {
- rewriteName, ok := c.Get("rewriteConfigFileName")
+ rewriteName, ok := c.Get("rewriteConfigFileName")
- name := c.Param("name")
+ name := c.Param("name")
- // for modify filename
- if ok {
- name = rewriteName.(string)
- }
+ // for modify filename
+ if ok {
+ name = rewriteName.(string)
+ }
- path := nginx.GetConfPath("sites-available", name)
+ path := nginx.GetConfPath("sites-available", name)
- enabled := true
- if _, err := os.Stat(nginx.GetConfPath("sites-enabled", name)); os.IsNotExist(err) {
- enabled = false
- }
+ enabled := true
+ if _, err := os.Stat(nginx.GetConfPath("sites-enabled", name)); os.IsNotExist(err) {
+ enabled = false
+ }
- c.Set("maybe_error", "nginx_config_syntax_error")
- config, err := nginx.ParseNgxConfig(path)
+ c.Set("maybe_error", "nginx_config_syntax_error")
+ config, err := nginx.ParseNgxConfig(path)
- if err != nil {
- ErrHandler(c, err)
- return
- }
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
- c.Set("maybe_error", "")
+ c.Set("maybe_error", "")
- certInfoMap := make(map[int]CertificateInfo)
- for serverIdx, server := range config.Servers {
- for _, directive := range server.Directives {
- if directive.Directive == "ssl_certificate" {
+ certInfoMap := make(map[int]CertificateInfo)
+ for serverIdx, server := range config.Servers {
+ for _, directive := range server.Directives {
+ if directive.Directive == "ssl_certificate" {
- pubKey, err := cert.GetCertInfo(directive.Params)
+ pubKey, err := cert.GetCertInfo(directive.Params)
- if err != nil {
- log.Println("Failed to get certificate information", err)
- break
- }
+ if err != nil {
+ log.Println("Failed to get certificate information", err)
+ break
+ }
- certInfoMap[serverIdx] = CertificateInfo{
- SubjectName: pubKey.Subject.CommonName,
- IssuerName: pubKey.Issuer.CommonName,
- NotAfter: pubKey.NotAfter,
- NotBefore: pubKey.NotBefore,
- }
+ certInfoMap[serverIdx] = CertificateInfo{
+ SubjectName: pubKey.Subject.CommonName,
+ IssuerName: pubKey.Issuer.CommonName,
+ NotAfter: pubKey.NotAfter,
+ NotBefore: pubKey.NotBefore,
+ }
- break
- }
- }
- }
+ break
+ }
+ }
+ }
- certModel, _ := model.FirstCert(name)
+ certModel, _ := model.FirstCert(name)
- c.Set("maybe_error", "nginx_config_syntax_error")
+ c.Set("maybe_error", "nginx_config_syntax_error")
- c.JSON(http.StatusOK, gin.H{
- "enabled": enabled,
- "name": name,
- "config": config.FmtCode(),
- "tokenized": config,
- "auto_cert": certModel.AutoCert == model.AutoCertEnabled,
- "cert_info": certInfoMap,
- })
+ c.JSON(http.StatusOK, gin.H{
+ "enabled": enabled,
+ "name": name,
+ "config": config.FmtCode(),
+ "tokenized": config,
+ "auto_cert": certModel.AutoCert == model.AutoCertEnabled,
+ "cert_info": certInfoMap,
+ })
}
func SaveDomain(c *gin.Context) {
- name := c.Param("name")
-
- 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" binding:"required"`
- Overwrite bool `json:"overwrite"`
- }
-
- if !BindAndValid(c, &json) {
- return
- }
-
- path := nginx.GetConfPath("sites-available", name)
-
- if !json.Overwrite && helper.FileExists(path) {
- c.JSON(http.StatusNotAcceptable, gin.H{
- "message": "File exists",
- })
- return
- }
-
- err := os.WriteFile(path, []byte(json.Content), 0644)
- if err != nil {
- ErrHandler(c, err)
- return
- }
- enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
- // rename the config file if needed
- if name != json.Name {
- newPath := nginx.GetConfPath("sites-available", json.Name)
- // check if dst file exists, do not rename
- if helper.FileExists(newPath) {
- c.JSON(http.StatusNotAcceptable, gin.H{
- "message": "File exists",
- })
- return
- }
- // recreate soft link
- if helper.FileExists(enabledConfigFilePath) {
- _ = os.Remove(enabledConfigFilePath)
- enabledConfigFilePath = nginx.GetConfPath("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 = nginx.GetConfPath("sites-enabled", name)
- if helper.FileExists(enabledConfigFilePath) {
- // Test nginx configuration
- output := nginx.TestConf()
- if nginx.GetLogLevel(output) >= nginx.Warn {
- c.JSON(http.StatusInternalServerError, gin.H{
- "message": output,
- "error": "nginx_config_syntax_error",
- })
- return
- }
-
- output = nginx.Reload()
-
- if nginx.GetLogLevel(output) >= nginx.Warn {
- c.JSON(http.StatusInternalServerError, gin.H{
- "message": output,
- })
- return
- }
- }
-
- GetDomain(c)
+ name := c.Param("name")
+
+ 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" binding:"required"`
+ Overwrite bool `json:"overwrite"`
+ }
+
+ if !BindAndValid(c, &json) {
+ return
+ }
+
+ path := nginx.GetConfPath("sites-available", name)
+
+ if !json.Overwrite && helper.FileExists(path) {
+ c.JSON(http.StatusNotAcceptable, gin.H{
+ "message": "File exists",
+ })
+ return
+ }
+
+ err := os.WriteFile(path, []byte(json.Content), 0644)
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+ enabledConfigFilePath := nginx.GetConfPath("sites-enabled", name)
+ // rename the config file if needed
+ if name != json.Name {
+ newPath := nginx.GetConfPath("sites-available", json.Name)
+ // check if dst file exists, do not rename
+ if helper.FileExists(newPath) {
+ c.JSON(http.StatusNotAcceptable, gin.H{
+ "message": "File exists",
+ })
+ return
+ }
+ // recreate soft link
+ if helper.FileExists(enabledConfigFilePath) {
+ _ = os.Remove(enabledConfigFilePath)
+ enabledConfigFilePath = nginx.GetConfPath("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 = nginx.GetConfPath("sites-enabled", name)
+ if helper.FileExists(enabledConfigFilePath) {
+ // Test nginx configuration
+ output := nginx.TestConf()
+ if nginx.GetLogLevel(output) >= nginx.Warn {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "message": output,
+ "error": "nginx_config_syntax_error",
+ })
+ return
+ }
+
+ output = nginx.Reload()
+
+ if nginx.GetLogLevel(output) >= nginx.Warn {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "message": output,
+ })
+ return
+ }
+ }
+
+ GetDomain(c)
}
func EnableDomain(c *gin.Context) {
- configFilePath := nginx.GetConfPath("sites-available", c.Param("name"))
- enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
-
- _, err := os.Stat(configFilePath)
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
- err = os.Symlink(configFilePath, enabledConfigFilePath)
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
- }
-
- // Test nginx config, if not pass then disable the site.
- output := nginx.TestConf()
-
- if nginx.GetLogLevel(output) >= nginx.Warn {
- _ = os.Remove(enabledConfigFilePath)
- c.JSON(http.StatusInternalServerError, gin.H{
- "message": output,
- })
- return
- }
-
- output = nginx.Reload()
-
- if nginx.GetLogLevel(output) >= nginx.Warn {
- c.JSON(http.StatusInternalServerError, gin.H{
- "message": output,
- })
- return
- }
-
- c.JSON(http.StatusOK, gin.H{
- "message": "ok",
- })
+ configFilePath := nginx.GetConfPath("sites-available", c.Param("name"))
+ enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
+
+ _, err := os.Stat(configFilePath)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ if _, err = os.Stat(enabledConfigFilePath); os.IsNotExist(err) {
+ err = os.Symlink(configFilePath, enabledConfigFilePath)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+ }
+
+ // Test nginx config, if not pass then disable the site.
+ output := nginx.TestConf()
+
+ if nginx.GetLogLevel(output) >= nginx.Warn {
+ _ = os.Remove(enabledConfigFilePath)
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "message": output,
+ })
+ return
+ }
+
+ output = nginx.Reload()
+
+ if nginx.GetLogLevel(output) >= nginx.Warn {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "message": output,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "ok",
+ })
}
func DisableDomain(c *gin.Context) {
- enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
-
- _, err := os.Stat(enabledConfigFilePath)
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- err = os.Remove(enabledConfigFilePath)
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- // delete auto cert record
- certModel := model.Cert{Filename: c.Param("name")}
- err = certModel.Remove()
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- output := nginx.Reload()
-
- if nginx.GetLogLevel(output) >= nginx.Warn {
- c.JSON(http.StatusInternalServerError, gin.H{
- "message": output,
- })
- return
- }
-
- c.JSON(http.StatusOK, gin.H{
- "message": "ok",
- })
+ enabledConfigFilePath := nginx.GetConfPath("sites-enabled", c.Param("name"))
+
+ _, err := os.Stat(enabledConfigFilePath)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ err = os.Remove(enabledConfigFilePath)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ // delete auto cert record
+ certModel := model.Cert{Filename: c.Param("name")}
+ err = certModel.Remove()
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ output := nginx.Reload()
+
+ if nginx.GetLogLevel(output) >= nginx.Warn {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "message": output,
+ })
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "ok",
+ })
}
func DeleteDomain(c *gin.Context) {
- var err error
- name := c.Param("name")
- availablePath := nginx.GetConfPath("sites-available", name)
- enabledPath := nginx.GetConfPath("sites-enabled", name)
-
- if _, err = os.Stat(availablePath); os.IsNotExist(err) {
- c.JSON(http.StatusNotFound, gin.H{
- "message": "site not found",
- })
- return
- }
-
- if _, err = os.Stat(enabledPath); err == nil {
- c.JSON(http.StatusNotAcceptable, gin.H{
- "message": "site is enabled",
- })
- return
- }
-
- certModel := model.Cert{Filename: name}
- _ = certModel.Remove()
-
- err = os.Remove(availablePath)
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- c.JSON(http.StatusOK, gin.H{
- "message": "ok",
- })
+ var err error
+ name := c.Param("name")
+ availablePath := nginx.GetConfPath("sites-available", name)
+ enabledPath := nginx.GetConfPath("sites-enabled", name)
+
+ if _, err = os.Stat(availablePath); os.IsNotExist(err) {
+ c.JSON(http.StatusNotFound, gin.H{
+ "message": "site not found",
+ })
+ return
+ }
+
+ if _, err = os.Stat(enabledPath); err == nil {
+ c.JSON(http.StatusNotAcceptable, gin.H{
+ "message": "site is enabled",
+ })
+ return
+ }
+
+ certModel := model.Cert{Filename: name}
+ _ = certModel.Remove()
+
+ err = os.Remove(availablePath)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "ok",
+ })
}
func AddDomainToAutoCert(c *gin.Context) {
- name := c.Param("name")
- certModel, err := model.FirstOrCreateCert(name)
+ name := c.Param("name")
- if err != nil {
- ErrHandler(c, err)
- return
- }
+ var json struct {
+ Domains []string `json:"domains"`
+ }
- err = certModel.Updates(&model.Cert{
- Name: name,
- AutoCert: model.AutoCertEnabled,
- })
+ if !BindAndValid(c, &json) {
+ return
+ }
- if err != nil {
- ErrHandler(c, err)
- return
- }
+ certModel, err := model.FirstOrCreateCert(name)
- c.JSON(http.StatusOK, certModel)
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ err = certModel.Updates(&model.Cert{
+ Name: name,
+ Domains: json.Domains,
+ AutoCert: model.AutoCertEnabled,
+ })
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ c.JSON(http.StatusOK, certModel)
}
func RemoveDomainFromAutoCert(c *gin.Context) {
- name := c.Param("name")
- certModel, err := model.FirstCert(name)
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
-
- err = certModel.Updates(&model.Cert{
- AutoCert: model.AutoCertDisabled,
- })
-
- if err != nil {
- ErrHandler(c, err)
- return
- }
- c.JSON(http.StatusOK, nil)
+ name := c.Param("name")
+ certModel, err := model.FirstCert(name)
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+
+ err = certModel.Updates(&model.Cert{
+ AutoCert: model.AutoCertDisabled,
+ })
+
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
+ c.JSON(http.StatusOK, nil)
}
func DuplicateSite(c *gin.Context) {
- name := c.Param("name")
+ name := c.Param("name")
- var json struct {
- Name string `json:"name" binding:"required"`
- }
+ var json struct {
+ Name string `json:"name" binding:"required"`
+ }
- if !BindAndValid(c, &json) {
- return
- }
+ if !BindAndValid(c, &json) {
+ return
+ }
- src := nginx.GetConfPath("sites-available", name)
- dst := nginx.GetConfPath("sites-available", json.Name)
+ src := nginx.GetConfPath("sites-available", name)
+ dst := nginx.GetConfPath("sites-available", json.Name)
- if helper.FileExists(dst) {
- c.JSON(http.StatusNotAcceptable, gin.H{
- "message": "File exists",
- })
- return
- }
+ if helper.FileExists(dst) {
+ c.JSON(http.StatusNotAcceptable, gin.H{
+ "message": "File exists",
+ })
+ return
+ }
- _, err := helper.CopyFile(src, dst)
+ _, err := helper.CopyFile(src, dst)
- if err != nil {
- ErrHandler(c, err)
- return
- }
+ if err != nil {
+ ErrHandler(c, err)
+ return
+ }
- c.JSON(http.StatusOK, gin.H{
- "dst": dst,
- })
+ c.JSON(http.StatusOK, gin.H{
+ "dst": dst,
+ })
}
diff --git a/server/model/cert.go b/server/model/cert.go
index f5186da64..2cc9f5e40 100644
--- a/server/model/cert.go
+++ b/server/model/cert.go
@@ -1,97 +1,101 @@
package model
import (
- "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
- "github.com/lib/pq"
- "os"
+ "github.com/0xJacky/Nginx-UI/server/pkg/nginx"
+ "github.com/lib/pq"
+ "os"
)
const (
- AutoCertEnabled = 1
- AutoCertDisabled = -1
+ AutoCertEnabled = 1
+ AutoCertDisabled = -1
)
type CertDomains []string
type Cert struct {
- Model
- Name string `json:"name"`
- Domains pq.StringArray `json:"domains" gorm:"type:text[]"`
- Filename string `json:"filename"`
- SSLCertificatePath string `json:"ssl_certificate_path"`
- SSLCertificateKeyPath string `json:"ssl_certificate_key_path"`
- AutoCert int `json:"auto_cert"`
- Log string `json:"log"`
+ Model
+ Name string `json:"name"`
+ Domains pq.StringArray `json:"domains" gorm:"type:text[]"`
+ Filename string `json:"filename"`
+ SSLCertificatePath string `json:"ssl_certificate_path"`
+ SSLCertificateKeyPath string `json:"ssl_certificate_key_path"`
+ AutoCert int `json:"auto_cert"`
+ Log string `json:"log"`
}
func FirstCert(confName string) (c Cert, err error) {
- err = db.First(&c, &Cert{
- Filename: confName,
- }).Error
+ err = db.First(&c, &Cert{
+ Filename: confName,
+ }).Error
- return
+ return
}
func FirstOrCreateCert(confName string) (c Cert, err error) {
- err = db.FirstOrCreate(&c, &Cert{Filename: confName}).Error
- return
+ err = db.FirstOrCreate(&c, &Cert{Filename: confName}).Error
+ return
}
func (c *Cert) Insert() error {
- return db.Create(c).Error
+ return db.Create(c).Error
}
func GetAutoCertList() (c []*Cert) {
- var t []*Cert
- db.Where("auto_cert", AutoCertEnabled).Find(&t)
+ var t []*Cert
+ db.Where("auto_cert", AutoCertEnabled).Find(&t)
- // check if this domain is enabled
- enabledConfig, err := os.ReadDir(nginx.GetConfPath("sites-enabled"))
+ // check if this domain is enabled
+ enabledConfig, err := os.ReadDir(nginx.GetConfPath("sites-enabled"))
- if err != nil {
- return
- }
+ if err != nil {
+ return
+ }
- enabledConfigMap := make(map[string]bool)
- for i := range enabledConfig {
- enabledConfigMap[enabledConfig[i].Name()] = true
- }
+ enabledConfigMap := make(map[string]bool)
+ for i := range enabledConfig {
+ enabledConfigMap[enabledConfig[i].Name()] = true
+ }
- for _, v := range t {
- if enabledConfigMap[v.Filename] == true {
- c = append(c, v)
- }
- }
+ for _, v := range t {
+ if enabledConfigMap[v.Filename] == true {
+ c = append(c, v)
+ }
+ }
- 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
+ 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
+ err = db.First(&c, id).Error
- return
+ return
}
func (c *Cert) Updates(n *Cert) error {
- return db.Model(&Cert{}).Where("id", c.ID).Updates(n).Error
+ return db.Model(&Cert{}).Where("id", c.ID).Updates(n).Error
+}
+
+func (c *Cert) ClearLog() {
+ db.Model(&Cert{}).Where("id", c.ID).Update("log", "")
}
func (c *Cert) Remove() error {
- if c.Filename == "" {
- return db.Delete(c).Error
- }
+ if c.Filename == "" {
+ return db.Delete(c).Error
+ }
- return db.Where("filename", c.Filename).Delete(c).Error
+ return db.Where("filename", c.Filename).Delete(c).Error
}
diff --git a/server/pkg/cert/auto_cert.go b/server/pkg/cert/auto_cert.go
index ef26d106e..eb3d13603 100644
--- a/server/pkg/cert/auto_cert.go
+++ b/server/pkg/cert/auto_cert.go
@@ -1,117 +1,123 @@
package cert
import (
- "fmt"
- "github.com/0xJacky/Nginx-UI/server/model"
- "github.com/pkg/errors"
- "log"
- "time"
+ "fmt"
+ "github.com/0xJacky/Nginx-UI/server/model"
+ "github.com/pkg/errors"
+ "log"
+ "time"
)
func handleIssueCertLogChan(logChan chan string) {
- defer func() {
- if err := recover(); err != nil {
- log.Println("[Auto Cert] handleIssueCertLogChan", err)
- }
- }()
-
- for logString := range logChan {
- log.Println("[Auto Cert] Info", logString)
- }
+ defer func() {
+ if err := recover(); err != nil {
+ log.Println("[Auto Cert] handleIssueCertLogChan", err)
+ }
+ }()
+
+ for logString := range logChan {
+ log.Println("[Auto Cert] Info", logString)
+ }
}
type AutoCertErrorLog struct {
- buffer []string
- cert *model.Cert
+ buffer []string
+ cert *model.Cert
}
func (t *AutoCertErrorLog) SetCertModel(cert *model.Cert) {
- t.cert = cert
+ t.cert = cert
}
func (t *AutoCertErrorLog) Push(text string, err error) {
- t.buffer = append(t.buffer, text+" "+err.Error())
- log.Println("[AutoCert Error]", text, err)
+ t.buffer = append(t.buffer, text+" "+err.Error())
+ log.Println("[AutoCert Error]", text, err)
}
func (t *AutoCertErrorLog) Exit(text string, err error) {
- t.buffer = append(t.buffer, text+" "+err.Error())
- log.Println("[AutoCert Error]", text, err)
+ t.buffer = append(t.buffer, text+" "+err.Error())
+ log.Println("[AutoCert Error]", text, err)
- if t.cert == nil {
- return
- }
+ if t.cert == nil {
+ return
+ }
- _ = t.cert.Updates(&model.Cert{
- Log: t.ToString(),
- })
+ _ = t.cert.Updates(&model.Cert{
+ Log: t.ToString(),
+ })
}
func (t *AutoCertErrorLog) ToString() (content string) {
- for _, v := range t.buffer {
- content += fmt.Sprintf("[AutoCert Error] %s\n", v)
- }
+ for _, v := range t.buffer {
+ content += fmt.Sprintf("[AutoCert Error] %s\n", v)
+ }
- return
+ return
}
func AutoObtain() {
- defer func() {
- if err := recover(); err != nil {
- log.Println("[AutoCert] Recover", err)
- }
- }()
- log.Println("[AutoCert] Start")
- autoCertList := model.GetAutoCertList()
- for _, certModel := range autoCertList {
- confName := certModel.Filename
-
- errLog := &AutoCertErrorLog{}
- errLog.SetCertModel(certModel)
-
- if len(certModel.Filename) == 0 {
- errLog.Exit("", errors.New("filename is empty"))
- continue
- }
-
- if len(certModel.Domains) == 0 {
- errLog.Exit(confName, errors.New("domains list is empty, "+
- "try to reopen auto-cert for this config:"+confName))
- continue
- }
-
- if certModel.SSLCertificatePath != "" {
- cert, err := GetCertInfo(certModel.SSLCertificatePath)
- if err != nil {
- errLog.Push("get cert info", err)
- // Get certificate info error, ignore this domain
- continue
- }
- // every week
- if time.Now().Sub(cert.NotBefore).Hours()/24 < 7 {
- continue
- }
- }
- // after 1 mo, reissue certificate
- logChan := make(chan string, 1)
- errChan := make(chan error, 1)
-
- // support SAN certification
- go IssueCert(certModel.Domains, logChan, errChan)
-
- go handleIssueCertLogChan(logChan)
-
- // block, unless errChan closed
- for err := range errChan {
- errLog.Push("issue cert", err)
- }
- // store error log to db
- _ = certModel.Updates(&model.Cert{
- Log: errLog.ToString(),
- })
-
- close(logChan)
- }
- log.Println("[AutoCert] End")
+ defer func() {
+ if err := recover(); err != nil {
+ log.Println("[AutoCert] Recover", err)
+ }
+ }()
+ log.Println("[AutoCert] Start")
+ autoCertList := model.GetAutoCertList()
+ for _, certModel := range autoCertList {
+ confName := certModel.Filename
+
+ errLog := &AutoCertErrorLog{}
+ errLog.SetCertModel(certModel)
+
+ if len(certModel.Filename) == 0 {
+ errLog.Exit("", errors.New("filename is empty"))
+ continue
+ }
+
+ if len(certModel.Domains) == 0 {
+ errLog.Exit(confName, errors.New("domains list is empty, "+
+ "try to reopen auto-cert for this config:"+confName))
+ continue
+ }
+
+ if certModel.SSLCertificatePath != "" {
+ cert, err := GetCertInfo(certModel.SSLCertificatePath)
+ if err != nil {
+ errLog.Push("get cert info", err)
+ // Get certificate info error, ignore this domain
+ continue
+ }
+ // every week
+ if time.Now().Sub(cert.NotBefore).Hours()/24 < 7 {
+ continue
+ }
+ }
+ // after 1 mo, reissue certificate
+ logChan := make(chan string, 1)
+ errChan := make(chan error, 1)
+
+ // support SAN certification
+ go IssueCert(certModel.Domains, logChan, errChan)
+
+ go handleIssueCertLogChan(logChan)
+
+ // block, unless errChan closed
+ for err := range errChan {
+ errLog.Push("issue cert", err)
+ }
+
+ logStr := errLog.ToString()
+ if logStr != "" {
+ // store error log to db
+ _ = certModel.Updates(&model.Cert{
+ Log: errLog.ToString(),
+ })
+ } else {
+ certModel.ClearLog()
+ }
+
+ close(logChan)
+ }
+ log.Println("[AutoCert] End")
}
diff --git a/server/pkg/cert/cert.go b/server/pkg/cert/cert.go
index ff7c9f50a..3da220560 100644
--- a/server/pkg/cert/cert.go
+++ b/server/pkg/cert/cert.go
@@ -92,7 +92,7 @@ func IssueCert(domain []string, logChan chan string, errChan chan error) {
)
if err != nil {
- errChan <- errors.Wrap(err, "issue cert challenge fail")
+ errChan <- errors.Wrap(err, "fail to challenge")
return
}
@@ -100,7 +100,7 @@ func IssueCert(domain []string, logChan chan string, errChan chan error) {
logChan <- "Registering user"
reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
- errChan <- errors.Wrap(err, "issue cert register fail")
+ errChan <- errors.Wrap(err, "fail to register")
return
}
myUser.Registration = reg
@@ -113,7 +113,7 @@ func IssueCert(domain []string, logChan chan string, errChan chan error) {
logChan <- "Obtaining certificate"
certificates, err := client.Certificate.Obtain(request)
if err != nil {
- errChan <- errors.Wrap(err, "issue cert fail to obtain")
+ errChan <- errors.Wrap(err, "fail to obtain")
return
}
name := strings.Join(domain, "_")
@@ -121,7 +121,7 @@ func IssueCert(domain []string, logChan chan string, errChan chan error) {
if _, err = os.Stat(saveDir); os.IsNotExist(err) {
err = os.MkdirAll(saveDir, 0755)
if err != nil {
- errChan <- errors.Wrap(err, "issue cert fail to create")
+ errChan <- errors.Wrap(err, "fail to mkdir")
return
}
}
@@ -142,7 +142,7 @@ func IssueCert(domain []string, logChan chan string, errChan chan error) {
certificates.PrivateKey, 0644)
if err != nil {
- errChan <- errors.Wrap(err, "error issue cert write key")
+ errChan <- errors.Wrap(err, "fail to write key")
return
}
@@ -152,4 +152,6 @@ func IssueCert(domain []string, logChan chan string, errChan chan error) {
nginx.Reload()
logChan <- "Finished"
+
+ close(logChan)
}
From 85dbbcc54b9e47e1b1f72929dc40b065ca86440a Mon Sep 17 00:00:00 2001
From: 0xJacky
Date: Wed, 15 Feb 2023 13:00:06 +0800
Subject: [PATCH 5/6] chore: update translations
---
frontend/package.json | 2 +-
frontend/src/language/en/app.po | 92 +++++---
frontend/src/language/messages.pot | 87 +++++---
frontend/src/language/translations.json | 2 +-
frontend/src/language/zh_CN/app.mo | Bin 13282 -> 13853 bytes
frontend/src/language/zh_CN/app.po | 93 +++++---
frontend/src/language/zh_TW/app.mo | Bin 13377 -> 13911 bytes
frontend/src/language/zh_TW/app.po | 93 +++++---
frontend/src/version.json | 2 +-
frontend/version.json | 2 +-
server/pkg/cert/cert.go | 278 ++++++++++++------------
11 files changed, 385 insertions(+), 266 deletions(-)
diff --git a/frontend/package.json b/frontend/package.json
index f1b04bdce..542a67459 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "nginx-ui-frontend-next",
"private": true,
- "version": "1.7.5",
+ "version": "1.7.6",
"type": "commonjs",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/language/en/app.po b/frontend/src/language/en/app.po
index 1eff1d907..c34f73157 100644
--- a/frontend/src/language/en/app.po
+++ b/frontend/src/language/en/app.po
@@ -17,8 +17,8 @@ msgstr "About"
msgid "Access Logs"
msgstr ""
-#: 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/cert/Cert.vue:74 src/views/config/config.ts:36
+#: src/views/domain/DomainList.vue:48 src/views/user/User.vue:43
msgid "Action"
msgstr "Action"
@@ -85,11 +85,11 @@ msgstr ""
msgid "Auto"
msgstr ""
-#: src/views/cert/Cert.vue:41 src/views/domain/cert/ChangeCert.vue:35
+#: src/views/cert/Cert.vue:37 src/views/domain/cert/ChangeCert.vue:35
msgid "Auto Cert"
msgstr ""
-#: src/views/cert/Cert.vue:8
+#: src/views/cert/Cert.vue:9
msgid "Auto cert is enabled, please do not modify this certification."
msgstr ""
@@ -97,6 +97,10 @@ msgstr ""
msgid "Auto Refresh"
msgstr ""
+#: src/views/cert/Cert.vue:27
+msgid "Auto-Cert Log"
+msgstr ""
+
#: src/views/domain/cert/IssueCert.vue:71
msgid "Auto-renewal disabled for %{name}"
msgstr "Auto-renewal disabled for %{name}"
@@ -152,7 +156,7 @@ msgstr "Certificate has expired"
msgid "Certificate is valid"
msgstr "Certificate is valid"
-#: src/views/cert/Cert.vue:12 src/views/domain/cert/Cert.vue:35
+#: src/views/cert/Cert.vue:34 src/views/domain/cert/Cert.vue:35
msgid "Certificate Status"
msgstr "Certificate Status"
@@ -176,10 +180,15 @@ msgstr ""
#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:34
#: src/views/domain/ngx_conf/LocationEditor.vue:35
#: src/views/domain/ngx_conf/LocationEditor.vue:52
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:180
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:181
msgid "Comments"
msgstr "Comments"
+#: src/views/cert/Cert.vue:32
+#, fuzzy
+msgid "Config Name"
+msgstr "Configuration Name"
+
#: src/views/domain/ngx_conf/ConfigTemplate.vue:61
#, fuzzy
msgid "Config Templates"
@@ -258,7 +267,13 @@ msgstr "Dashboard"
msgid "Database (Optional, default: database)"
msgstr "Database (Optional, default: database)"
-#: src/components/StdDataDisplay/StdTable.vue:528
+#: src/components/StdDataDisplay/StdTable.vue:31
+#: src/components/StdDataDisplay/StdTable.vue:32
+#: src/components/StdDataDisplay/StdTable.vue:37
+#: src/components/StdDataDisplay/StdTable.vue:50
+#: src/components/StdDataDisplay/StdTable.vue:52
+#: src/components/StdDataDisplay/StdTable.vue:53
+#: src/components/StdDataDisplay/StdTable.vue:57
#: src/views/domain/DomainList.vue:19 src/views/domain/DomainList.vue:20
#: src/views/domain/DomainList.vue:21 src/views/domain/DomainList.vue:28
#: src/views/domain/DomainList.vue:32
@@ -269,7 +284,7 @@ msgstr ""
msgid "Delete ID: %{id}"
msgstr ""
-#: src/views/domain/DomainList.vue:81
+#: src/views/domain/DomainList.vue:82
msgid "Delete site: %{site_name}"
msgstr ""
@@ -304,15 +319,15 @@ msgstr "Directives"
msgid "Disable auto-renewal failed for %{name}"
msgstr "Disable auto-renewal failed for %{name}"
-#: src/views/cert/Cert.vue:51 src/views/domain/cert/ChangeCert.vue:45
+#: src/views/cert/Cert.vue:47 src/views/domain/cert/ChangeCert.vue:45
#: 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:16 src/views/domain/DomainList.vue:35
#: src/views/domain/DomainList.vue:7 src/views/domain/DomainList.vue:8
#: src/views/domain/DomainList.vue:9
msgid "Disabled"
msgstr "Disabled"
-#: src/views/domain/DomainEdit.vue:146 src/views/domain/DomainList.vue:69
+#: src/views/domain/DomainEdit.vue:146 src/views/domain/DomainList.vue:70
msgid "Disabled successfully"
msgstr "Disabled successfully"
@@ -320,14 +335,14 @@ msgstr "Disabled successfully"
msgid "Disk IO"
msgstr "Disk IO"
-#: src/views/cert/Cert.vue:32
-msgid "Domain"
-msgstr ""
-
#: src/views/domain/DomainAdd.vue:58
msgid "Domain Config Created Successfully"
msgstr "Domain Config Created Successfully"
+#: src/views/cert/Cert.vue:21
+msgid "Domains list is empty, try to reopen auto-cert for %{config}"
+msgstr ""
+
#: src/language/constants.ts:26
msgid "Download latest release error"
msgstr ""
@@ -338,9 +353,15 @@ msgstr ""
#: src/views/domain/DomainList.vue:14 src/views/domain/DomainList.vue:15
#: src/views/domain/DomainList.vue:16 src/views/domain/DomainList.vue:23
+#: src/views/domain/SiteDuplicate.vue:2
msgid "Duplicate"
msgstr ""
+#: src/views/domain/SiteDuplicate.vue:43
+#, fuzzy
+msgid "Duplicated successfully"
+msgstr "Saved successfully"
+
#: src/views/domain/DomainEdit.vue:4 src/views/domain/DomainEdit.vue:5
msgid "Edit %{n}"
msgstr "Edit %{n}"
@@ -369,16 +390,16 @@ msgstr "Enable failed"
msgid "Enable TLS"
msgstr "Enable TLS"
-#: src/views/cert/Cert.vue:48 src/views/domain/cert/ChangeCert.vue:42
+#: src/views/cert/Cert.vue:44 src/views/domain/cert/ChangeCert.vue:42
#: src/views/domain/DomainEdit.vue:43 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
+#: src/views/domain/DomainList.vue:19 src/views/domain/DomainList.vue:32
msgid "Enabled"
msgstr "Enabled"
#: src/views/domain/DomainAdd.vue:47 src/views/domain/DomainEdit.vue:137
-#: src/views/domain/DomainList.vue:59
+#: src/views/domain/DomainList.vue:60
msgid "Enabled successfully"
msgstr "Enabled successfully"
@@ -410,11 +431,11 @@ msgstr "Expiration Date: %{date}"
msgid "Export"
msgstr ""
-#: src/views/domain/DomainEdit.vue:149 src/views/domain/DomainList.vue:73
+#: src/views/domain/DomainEdit.vue:149 src/views/domain/DomainList.vue:74
msgid "Failed to disable %{msg}"
msgstr "Failed to disable %{msg}"
-#: src/views/domain/DomainEdit.vue:140 src/views/domain/DomainList.vue:63
+#: src/views/domain/DomainEdit.vue:140 src/views/domain/DomainList.vue:64
msgid "Failed to enable %{msg}"
msgstr "Failed to enable %{msg}"
@@ -609,7 +630,7 @@ msgstr "Single Directive"
#: src/views/cert/Cert.vue:16 src/views/config/config.ts:9
#: src/views/domain/cert/ChangeCert.vue:19 src/views/domain/DomainEdit.vue:46
-#: src/views/domain/DomainList.vue:15
+#: src/views/domain/DomainList.vue:16 src/views/domain/SiteDuplicate.vue:5
msgid "Name"
msgstr "Name"
@@ -736,6 +757,12 @@ msgstr ""
msgid "Performing core upgrade"
msgstr ""
+#: src/views/domain/SiteDuplicate.vue:28
+msgid ""
+"Please input name, this will be used as the filename of the new "
+"configuration!"
+msgstr ""
+
#: src/views/other/Install.vue:36
msgid "Please input your E-mail!"
msgstr "Please input your E-mail!"
@@ -885,9 +912,8 @@ msgstr "Send"
#: src/components/StdDataDisplay/StdTable.vue:168
#: src/components/StdDataDisplay/StdTable.vue:343
#: src/components/StdDataDisplay/StdTable.vue:463
-#: src/views/config/ConfigEdit.vue:32 src/views/domain/DomainEdit.vue:87
-#: src/views/domain/DomainList.vue:83 src/views/other/Install.vue:71
-#: src/views/preference/Preference.vue:41
+#: src/views/config/ConfigEdit.vue:32 src/views/domain/DomainList.vue:84
+#: src/views/other/Install.vue:71 src/views/preference/Preference.vue:41
msgid "Server error"
msgstr "Server error"
@@ -917,27 +943,27 @@ msgstr "Sites List"
msgid "Sites List"
msgstr "Sites List"
-#: src/views/cert/Cert.vue:65
+#: src/views/cert/Cert.vue:61
#, fuzzy
msgid "SSL Certificate Key Path"
msgstr "Certificate Status"
-#: src/views/cert/Cert.vue:58
+#: src/views/cert/Cert.vue:54
#, fuzzy
msgid "SSL Certificate Path"
msgstr "Certificate Status"
-#: src/views/cert/Cert.vue:19
+#: src/views/cert/Cert.vue:41
#, fuzzy
msgid "SSL Certification Content"
msgstr "Certificate Status"
-#: src/views/cert/Cert.vue:22
+#: src/views/cert/Cert.vue:44
#, fuzzy
msgid "SSL Certification Key Content"
msgstr "Certificate Status"
-#: src/views/domain/DomainList.vue:24
+#: src/views/domain/DomainList.vue:25
msgid "Status"
msgstr "Status"
@@ -1003,12 +1029,16 @@ msgstr ""
msgid "Theme"
msgstr ""
+#: src/views/cert/Cert.vue:15
+msgid "This auto-cert item is invalid, please remove it."
+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/cert/Cert.vue:68 src/views/config/config.ts:29
+#: src/views/domain/DomainList.vue:42 src/views/user/User.vue:37
msgid "Updated at"
msgstr "Updated at"
diff --git a/frontend/src/language/messages.pot b/frontend/src/language/messages.pot
index 63fecf1bd..7d25bcac7 100644
--- a/frontend/src/language/messages.pot
+++ b/frontend/src/language/messages.pot
@@ -11,9 +11,9 @@ msgstr ""
msgid "Access Logs"
msgstr ""
-#: src/views/cert/Cert.vue:78
+#: src/views/cert/Cert.vue:74
#: src/views/config/config.ts:36
-#: src/views/domain/DomainList.vue:47
+#: src/views/domain/DomainList.vue:48
#: src/views/user/User.vue:43
msgid "Action"
msgstr ""
@@ -83,12 +83,12 @@ msgstr ""
msgid "Auto"
msgstr ""
-#: src/views/cert/Cert.vue:41
+#: src/views/cert/Cert.vue:37
#: src/views/domain/cert/ChangeCert.vue:35
msgid "Auto Cert"
msgstr ""
-#: src/views/cert/Cert.vue:8
+#: src/views/cert/Cert.vue:9
msgid "Auto cert is enabled, please do not modify this certification."
msgstr ""
@@ -96,6 +96,10 @@ msgstr ""
msgid "Auto Refresh"
msgstr ""
+#: src/views/cert/Cert.vue:27
+msgid "Auto-Cert Log"
+msgstr ""
+
#: src/views/domain/cert/IssueCert.vue:71
msgid "Auto-renewal disabled for %{name}"
msgstr ""
@@ -153,7 +157,7 @@ msgstr ""
msgid "Certificate is valid"
msgstr ""
-#: src/views/cert/Cert.vue:12
+#: src/views/cert/Cert.vue:34
#: src/views/domain/cert/Cert.vue:35
msgid "Certificate Status"
msgstr ""
@@ -179,10 +183,14 @@ msgstr ""
#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:34
#: src/views/domain/ngx_conf/LocationEditor.vue:35
#: src/views/domain/ngx_conf/LocationEditor.vue:52
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:180
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:181
msgid "Comments"
msgstr ""
+#: src/views/cert/Cert.vue:32
+msgid "Config Name"
+msgstr ""
+
#: src/views/domain/ngx_conf/ConfigTemplate.vue:61
msgid "Config Templates"
msgstr ""
@@ -263,7 +271,13 @@ msgstr ""
msgid "Database (Optional, default: database)"
msgstr ""
-#: src/components/StdDataDisplay/StdTable.vue:528
+#: src/components/StdDataDisplay/StdTable.vue:31
+#: src/components/StdDataDisplay/StdTable.vue:32
+#: src/components/StdDataDisplay/StdTable.vue:37
+#: src/components/StdDataDisplay/StdTable.vue:50
+#: src/components/StdDataDisplay/StdTable.vue:52
+#: src/components/StdDataDisplay/StdTable.vue:53
+#: src/components/StdDataDisplay/StdTable.vue:57
#: src/views/domain/DomainList.vue:19
#: src/views/domain/DomainList.vue:20
#: src/views/domain/DomainList.vue:21
@@ -276,7 +290,7 @@ msgstr ""
msgid "Delete ID: %{id}"
msgstr ""
-#: src/views/domain/DomainList.vue:81
+#: src/views/domain/DomainList.vue:82
msgid "Delete site: %{site_name}"
msgstr ""
@@ -312,12 +326,12 @@ msgstr ""
msgid "Disable auto-renewal failed for %{name}"
msgstr ""
-#: src/views/cert/Cert.vue:51
+#: src/views/cert/Cert.vue:47
#: src/views/domain/cert/ChangeCert.vue:45
#: 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:35
#: src/views/domain/DomainList.vue:7
#: src/views/domain/DomainList.vue:8
#: src/views/domain/DomainList.vue:9
@@ -325,7 +339,7 @@ msgid "Disabled"
msgstr ""
#: src/views/domain/DomainEdit.vue:146
-#: src/views/domain/DomainList.vue:69
+#: src/views/domain/DomainList.vue:70
msgid "Disabled successfully"
msgstr ""
@@ -333,14 +347,14 @@ msgstr ""
msgid "Disk IO"
msgstr ""
-#: src/views/cert/Cert.vue:32
-msgid "Domain"
-msgstr ""
-
#: src/views/domain/DomainAdd.vue:58
msgid "Domain Config Created Successfully"
msgstr ""
+#: src/views/cert/Cert.vue:21
+msgid "Domains list is empty, try to reopen auto-cert for %{config}"
+msgstr ""
+
#: src/language/constants.ts:26
msgid "Download latest release error"
msgstr ""
@@ -353,9 +367,14 @@ msgstr ""
#: src/views/domain/DomainList.vue:15
#: src/views/domain/DomainList.vue:16
#: src/views/domain/DomainList.vue:23
+#: src/views/domain/SiteDuplicate.vue:2
msgid "Duplicate"
msgstr ""
+#: src/views/domain/SiteDuplicate.vue:43
+msgid "Duplicated successfully"
+msgstr ""
+
#: src/views/domain/DomainEdit.vue:4
#: src/views/domain/DomainEdit.vue:5
msgid "Edit %{n}"
@@ -386,7 +405,7 @@ msgstr ""
msgid "Enable TLS"
msgstr ""
-#: src/views/cert/Cert.vue:48
+#: src/views/cert/Cert.vue:44
#: src/views/domain/cert/ChangeCert.vue:42
#: src/views/domain/DomainEdit.vue:43
#: src/views/domain/DomainEdit.vue:6
@@ -395,13 +414,13 @@ msgstr ""
#: src/views/domain/DomainList.vue:11
#: src/views/domain/DomainList.vue:12
#: src/views/domain/DomainList.vue:19
-#: src/views/domain/DomainList.vue:31
+#: src/views/domain/DomainList.vue:32
msgid "Enabled"
msgstr ""
#: src/views/domain/DomainAdd.vue:47
#: src/views/domain/DomainEdit.vue:137
-#: src/views/domain/DomainList.vue:59
+#: src/views/domain/DomainList.vue:60
msgid "Enabled successfully"
msgstr ""
@@ -437,12 +456,12 @@ msgid "Export"
msgstr ""
#: src/views/domain/DomainEdit.vue:149
-#: src/views/domain/DomainList.vue:73
+#: src/views/domain/DomainList.vue:74
msgid "Failed to disable %{msg}"
msgstr ""
#: src/views/domain/DomainEdit.vue:140
-#: src/views/domain/DomainList.vue:63
+#: src/views/domain/DomainList.vue:64
msgid "Failed to enable %{msg}"
msgstr ""
@@ -636,7 +655,8 @@ msgstr ""
#: src/views/config/config.ts:9
#: src/views/domain/cert/ChangeCert.vue:19
#: src/views/domain/DomainEdit.vue:46
-#: src/views/domain/DomainList.vue:15
+#: src/views/domain/DomainList.vue:16
+#: src/views/domain/SiteDuplicate.vue:5
msgid "Name"
msgstr ""
@@ -765,6 +785,10 @@ msgstr ""
msgid "Performing core upgrade"
msgstr ""
+#: src/views/domain/SiteDuplicate.vue:28
+msgid "Please input name, this will be used as the filename of the new configuration!"
+msgstr ""
+
#: src/views/other/Install.vue:36
msgid "Please input your E-mail!"
msgstr ""
@@ -924,8 +948,7 @@ msgstr ""
#: src/components/StdDataDisplay/StdTable.vue:343
#: src/components/StdDataDisplay/StdTable.vue:463
#: src/views/config/ConfigEdit.vue:32
-#: src/views/domain/DomainEdit.vue:87
-#: src/views/domain/DomainList.vue:83
+#: src/views/domain/DomainList.vue:84
#: src/views/other/Install.vue:71
#: src/views/preference/Preference.vue:41
msgid "Server error"
@@ -957,23 +980,23 @@ msgstr ""
msgid "Sites List"
msgstr ""
-#: src/views/cert/Cert.vue:65
+#: src/views/cert/Cert.vue:61
msgid "SSL Certificate Key Path"
msgstr ""
-#: src/views/cert/Cert.vue:58
+#: src/views/cert/Cert.vue:54
msgid "SSL Certificate Path"
msgstr ""
-#: src/views/cert/Cert.vue:19
+#: src/views/cert/Cert.vue:41
msgid "SSL Certification Content"
msgstr ""
-#: src/views/cert/Cert.vue:22
+#: src/views/cert/Cert.vue:44
msgid "SSL Certification Key Content"
msgstr ""
-#: src/views/domain/DomainList.vue:24
+#: src/views/domain/DomainList.vue:25
msgid "Status"
msgstr ""
@@ -1030,13 +1053,17 @@ msgstr ""
msgid "Theme"
msgstr ""
+#: src/views/cert/Cert.vue:15
+msgid "This auto-cert item is invalid, please remove it."
+msgstr ""
+
#: src/views/config/config.ts:14
msgid "Type"
msgstr ""
-#: src/views/cert/Cert.vue:72
+#: src/views/cert/Cert.vue:68
#: src/views/config/config.ts:29
-#: src/views/domain/DomainList.vue:41
+#: src/views/domain/DomainList.vue:42
#: src/views/user/User.vue:37
msgid "Updated at"
msgstr ""
diff --git a/frontend/src/language/translations.json b/frontend/src/language/translations.json
index 4bcc3df6e..afca28657 100644
--- a/frontend/src/language/translations.json
+++ b/frontend/src/language/translations.json
@@ -1 +1 @@
-{"en":{"About":"About","Action":"Action","Add Directive Below":"Add Directive Below","Add Location":"Add Location","Add Site":"Add Site","Advance Mode":"Advance Mode","Are you sure you want to remove this directive?":"Are you sure you want to remove this directive?","Auto-renewal disabled for %{name}":"Auto-renewal disabled for %{name}","Auto-renewal enabled for %{name}":"Auto-renewal enabled for %{name}","Back":"Back","Base information":"Base information","Basic Mode":"Basic Mode","Build with":"Build with","Cancel":"Cancel","Certificate has expired":"Certificate has expired","Certificate is valid":"Certificate is valid","Certificate Status":"Certificate Status","Comments":"Comments","Configuration Name":"Configuration Name","Configurations":"Configurations","Configure SSL":"Configure SSL","Content":"Content","CPU Status":"CPU Status","CPU:":"CPU:","Create Another":"Create Another","Created at":"Created at","Dashboard":"Dashboard","Database (Optional, default: database)":"Database (Optional, default: database)","Development Mode":"Development Mode","Directive":"Directive","Directives":"Directives","Disable auto-renewal failed for %{name}":"Disable auto-renewal failed for %{name}","Disabled":"Disabled","Disabled successfully":"Disabled successfully","Disk IO":"Disk IO","Domain Config Created Successfully":"Domain Config Created Successfully","Edit %{n}":"Edit %{n}","Edit Configuration":"Edit Configuration","Edit Site":"Edit Site","Email (*)":"Email (*)","Enable auto-renewal failed for %{name}":"Enable auto-renewal failed for %{name}","Enable failed":"Enable failed","Enable TLS":"Enable TLS","Enabled":"Enabled","Enabled successfully":"Enabled successfully","Encrypt website with Let's Encrypt":"Encrypt website with Let's Encrypt","Expiration Date: %{date}":"Expiration Date: %{date}","Failed to disable %{msg}":"Failed to disable %{msg}","Failed to enable %{msg}":"Failed to enable %{msg}","File Not Found":"File Not Found","Finished":"Finished","Getting the certificate, please wait...":"Getting the certificate, please wait...","Home":"Home","Install":"Install","Intermediate Certification Authorities: %{issuer}":"Intermediate Certification Authorities: %{issuer}","Leave blank for no change":"Leave blank for no change","Load Averages:":"Load Averages:","Location":"Location","Locations":"Locations","Login":"Login","Login successful":"Login successful","Logout successful":"Logout successful","Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.":"Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.","Manage Configs":"Manage Configs","Manage Sites":"Manage Sites","Manage Users":"Manage Users","Memory":"Memory","Memory and Storage":"Memory and Storage","Modify Config":"Modify Config","Name":"Name","Network":"Network","Network Statistics":"Network Statistics","Network Total Receive":"Network Total Receive","Network Total Send":"Network Total Send","Next":"Next","No":"No","Not Found":"Not Found","Not Valid Before: %{date}":"Not Valid Before: %{date}","OS:":"OS:","Params":"Params","Password":"Password","Password (*)":"Password (*)","Path":"Path","Please input your E-mail!":"Please input your E-mail!","Please input your password!":"Please input your password!","Please input your username!":"Please input your username!","Project Team":"Project Team","Reads":"Reads","Receive":"Receive","Save":"Save","Save Directive":"Save Directive","Save error %{msg}":"Save error %{msg}","Saved successfully":"Saved successfully","Send":"Send","Server error":"Server error","Server Info":"Server Info","server_name not found in directives":"server_name not found in directives","server_name parameter is required":"server_name parameter is required","Single Directive":"Single Directive","Sites List":"Sites List","Status":"Status","Storage":"Storage","Subject Name: %{name}":"Subject Name: %{name}","Swap":"Swap","Terminal":"Terminal","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.":"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.","The filename cannot contain the following characters: %{c}":"The filename cannot contain the following characters: %{c}","Updated at":"Updated at","Uptime:":"Uptime:","Username":"Username","Username (*)":"Username (*)","Warning":"Warning","Writes":"Writes","Yes":"Yes","License":{"Project":"License"}},"zh_TW":{"About":"關於","Access Logs":"訪問日誌","Action":"操作","Add":"新增","Add Directive Below":"在下面新增指令","Add Location":"新增 Location","Add Site":"新增站點","Advance Mode":"高階模式","Arch":"架搆","Are you sure you want to delete?":"你確定你要刪除?","Are you sure you want to remove this directive?":"您確定要刪除這條指令?","Are you sure you want to remove this location?":"您確定要刪除此 Location 嗎?","Author":"作者","Auto":"自動","Auto Cert":"自動更新","Auto cert is enabled, please do not modify this certification.":"自動證書已啟用,請不要修改此證書。","Auto Refresh":"自動刷新","Auto-renewal disabled for %{name}":"已關閉 %{name} 自動續簽","Auto-renewal enabled for %{name}":"已啟用 %{name} 自動續簽","Back":"返回","Back Home":"回到首頁","Base information":"基本訊息","Basic Mode":"基本模式","Batch Modify":"批量修改","Build with":"構建基於","Cancel":"取消","Certificate has expired":"此憑證已過期","Certificate is valid":"此憑證有效","Certificate Status":"憑證狀態","Certification":"證照","Change Certificate":"更改證書","Check again":"再次檢查","Comments":"註釋","Config Templates":"配置模板","Configuration file is test successful":"配置文件測試成功","Configuration Name":"配置名稱","Configurations":"配置","Configure SSL":"配置 SSL","Content":"內容","Core Upgrade":"核心升級","CPU Status":"中央處理器狀態","CPU:":"中央處理器:","Create Another":"再創建一個","Created at":"建立時間","Creating client facilitates communication with the CA server":"創建客戶端方便與CA服務器通信","Current Version":"當前版本","Custom":"自訂","Dark":"深色","Dashboard":"儀表盤","Database (Optional, default: database)":"資料庫 (可選,預設: database)","Delete":"刪除","Delete ID: %{id}":"刪除 ID: %{id}","Delete site: %{site_name}":"刪除站點:%{site_name}","Description":"敘述","Development Mode":"開發模式","Dir":"目錄","Directive":"指令","Directives":"指令","Disable auto-renewal failed for %{name}":"關閉 %{name} 自動續簽失敗","Disabled":"禁用","Disabled successfully":"禁用成功","Disk IO":"磁碟 IO","Domain":"網域","Domain Config Created Successfully":"域名配置文件創建成功","Download latest release error":"下載最新版本錯誤","Downloading latest release":"正在下載最新版本","Duplicate":"複製","Edit %{n}":"編輯 %{n}","Edit Configuration":"編輯配置","Edit Site":"編輯站點","Email (*)":"郵箱 (*)","Enable auto-renewal failed for %{name}":"啟用 %{name} 自動續簽失敗","Enable failed":"啟用失敗","Enable TLS":"啟用 TLS","Enabled":"啟用","Enabled successfully":"啟用成功","Encrypt website with Let's Encrypt":"用 Let's Encrypt 對網站進行加密","Error":"錯誤","Error Logs":"錯誤日志","Executable Path":"可執行檔路徑","Expiration Date: %{date}":"過期時間: %{date}","Export":"導出","Failed to disable %{msg}":"禁用失敗 %{msg}","Failed to enable %{msg}":"啟用失敗 %{msg}","Failed to get certificate information":"獲取證書信息失敗","Failed to save, syntax error(s) was detected in the configuration.":"保存失敗,在配置中檢測到語法錯誤。","File":"文件","File exists":"文件已存在","File Not Found":"未找到檔案","Filter":"篩選","Finished":"完成","Format Code":"格式化代碼","Format error %{msg}":"格式錯誤 %{msg}","Format successfully":"格式化成功","Generate":"生成","Generating private key for registering account":"生成註冊賬號私鑰","Getting the certificate, please wait...":"正在獲取憑證,請稍等...","Home":"首頁","HTTP Challenge Port":"HTTP 挑戰端口","HTTP Port":"HTTP 監聽埠","Initial core upgrader error":"初始核心升級程序錯誤","Initialing core upgrader":"正在初始化核心升級程序","Inspect Configurations":"檢查配置","Install":"安裝","Install successfully":"安裝成功","Intermediate Certification Authorities: %{issuer}":"中級憑證頒發機構: %{issuer}","Issued certificate successfully":"頒發證書成功","Jwt Secret":"Jwt Secret","Last checked at":"上次檢查時間","Leave blank for no change":"留空表示不修改","Light":"淺色","Load Averages:":"系統負載:","Location":"Location","Locations":"Locations","Login":"登入","Login successful":"登入成功","Logout successful":"登出成功","Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.":"在獲取證書前,請確保配置檔案中已將 .well-known 目錄反向代理到 HTTPChallengePort (預設: 9180)","Manage Configs":"配置管理","Manage Sites":"網站管理","Manage Users":"使用者管理","Memory":"記憶體","Memory and Storage":"記憶體和存儲","Modify":"修改","Modify Config":"修改配置","Multi-line Directive":"多行指令","Name":"名稱","Network":"網路","Network Statistics":"網路統計","Network Total Receive":"下載流量","Network Total Send":"上傳流量","New version released":"新版本發布","Next":"下一步","Nginx Access Log Path":"Nginx 訪問日誌路徑","Nginx Configuration Parse Error":"Nginx 配置解析錯誤","Nginx Control":"Nginx 控件","Nginx Error Log Path":"Nginx 錯誤日誌路徑","Nginx Log":"Nginx 日誌","Nginx reloaded successfully":"Nginx 重載成功","Nginx restarted successfully":"Nginx 重啟成功","No":"取消","Not Found":"找不到頁面","Not Valid Before: %{date}":"此前無效: %{date}","Note":"備註","Obtaining certificate":"正在獲取證書","OK":"確定","OS":"作業系統","OS:":"作業系統:","Params":"參數","Password":"密碼","Password (*)":"密碼 (*)","Path":"路徑","Perform core upgrade error":"執行核心升級錯誤","Performing core upgrade":"正在執行核心升級","Please input your E-mail!":"請輸入您的郵箱!","Please input your password!":"請輸入您的密碼!","Please input your username!":"請輸入您的使用者名稱!","Preference":"設定","Preparing lego configurations":"準備 Lego 配置","Prohibit changing root password in demo":"禁止在demo中修改root密碼","Prohibit deleting the default user":"禁止刪除默認用戶","Project Team":"專案團隊","Reads":"讀","Receive":"下載","Registering user":"註冊用戶","Reinstall":"安裝","Release Note":"發行公告","Reload":"重新載入","Reloading":"换弹中","Reloading nginx":"重载 Nginx","Reset":"重設","Restart":"重新啟動","Restarting":"重啟","Run Mode":"高階模式","Running":"運行中","Save":"儲存","Save Directive":"儲存指令","Save error %{msg}":"儲存錯誤 %{msg}","Save successfully":"保存成功","Save Successfully":"保存成功","Saved successfully":"儲存成功","Selector":"選擇器","Send":"上傳","Server error":"伺服器錯誤","Server Info":"伺服器資訊","server_name not found in directives":"未在指令集合中找到 server_name","server_name parameter is required":"必須為 server_name 指令指明參數","Single Directive":"單行指令","Site Logs":"網站日誌","Sites List":"站點列表","SSL Certificate Key Path":"SSL 證書密鑰路徑","SSL Certificate Path":"SSL證書路徑","SSL Certification Content":"SSL認證內容","SSL Certification Key Content":"SSL 證書密鑰內容","Status":"狀態","Stopped":"已停止","Storage":"儲存","Subject Name: %{name}":"主體名稱: %{name}","Swap":"交換空間","System":"系統","Table":"表格","Terminal":"終端","Terminal Start Command":"終端啟動命令","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.":"系統將會每小時檢測一次該域名證書,若距離上次簽發已超過1個月,則將自動續簽。
如果您之前沒有證書,請先點選「從 Let's Encrypt 獲取證書」。","The filename cannot contain the following characters: %{c}":"檔名不能包含以下字元: %{c}","The server_name in the current configuration must be the domain name you need to get the certificate.":"注意:當前配置中的 server_name 必須為需要申請證書的域名。","The username or password is incorrect":"用戶名或密碼不正確","Theme":"外觀樣式","Type":"類型","Updated at":"修改時間","Updated successfully":"已成功更新","Upgrade":"升級","Upgraded successfully":"升級成功","Upgrading Nginx UI, please wait...":"正在升級 Nginx UI,請稍候...","Uptime:":"執行時間:","Username":"使用者名稱","Username (*)":"使用者名稱 (*)","Using HTTP01 challenge provider":"使用 HTTP01 挑戰提供者","View":"查看","Warning":"警告","Writes":"寫","Writing certificate private key to disk":"將證書私鑰寫入磁盤","Writing certificate to disk":"將證書寫入磁盤","Yes":"是的","You are using the latest version":"您使用的是最新版本","You can check Nginx UI upgrade at this page.":"您可以在此頁面查看 Nginx UI 升級。","License":{"Project":"開源軟體授權條款"}},"zh_CN":{"About":"关于","Access Logs":"访问日志","Action":"操作","Add":"添加","Add Directive Below":"在下面添加指令","Add Location":"添加 Location","Add Site":"添加站点","Advance Mode":"高级模式","Arch":"架构","Are you sure you want to delete?":"您确定要删除吗?","Are you sure you want to remove this directive?":"您确定要删除这条指令?","Are you sure you want to remove this location?":"您确定要删除这个 Location?","Author":"作者","Auto":"自动","Auto Cert":"自动更新","Auto cert is enabled, please do not modify this certification.":"自动更新已启用,请勿修改此证书配置。","Auto Refresh":"自动刷新","Auto-renewal disabled for %{name}":"成功关闭 %{name} 自动续签","Auto-renewal enabled for %{name}":"成功启用 %{name} 自动续签","Back":"返回","Back Home":"返回首页","Base information":"基本信息","Basic Mode":"基本模式","Batch Modify":"批量修改","Build with":"构建基于","Cancel":"取消","Certificate has expired":"此证书已过期","Certificate is valid":"此证书有效","Certificate Status":"证书状态","Certification":"证书","Change Certificate":"更改证书","Check again":"重新检查","Comments":"注释","Config Templates":"配置","Configuration file is test successful":"配置文件测试成功","Configuration Name":"配置名称","Configurations":"配置","Configure SSL":"配置 SSL","Content":"内容","Core Upgrade":"核心升级","CPU Status":"CPU 状态","CPU:":"CPU:","Create Another":"再创建一个","Created at":"创建时间","Creating client facilitates communication with the CA server":"正在创建客户端用于与 CA 服务器通信","Current Version":"当前版本","Custom":"自定义","Dark":"深色","Dashboard":"仪表盘","Database (Optional, default: database)":"数据库 (可选,默认: database)","Delete":"删除","Delete ID: %{id}":"删除 ID: %{id}","Delete site: %{site_name}":"删除站点: %{site_name}","Description":"描述","Development Mode":"开发模式","Dir":"目录","Directive":"指令","Directives":"指令","Disable auto-renewal failed for %{name}":"关闭 %{name} 自动续签失败","Disabled":"禁用","Disabled successfully":"禁用成功","Disk IO":"磁盘 IO","Domain":"域名","Domain Config Created Successfully":"域名配置文件创建成功","Download latest release error":"下载最新版本错误","Downloading latest release":"下载最新版本","Duplicate":"复制","Edit %{n}":"编辑 %{n}","Edit Configuration":"编辑配置","Edit Site":"编辑站点","Email (*)":"邮箱 (*)","Enable auto-renewal failed for %{name}":"启用 %{name} 自动续签失败","Enable failed":"启用失败","Enable TLS":"启用 TLS","Enabled":"启用","Enabled successfully":"启用成功","Encrypt website with Let's Encrypt":"用 Let's Encrypt 对网站进行加密","Error":"错误","Error Logs":"错误日志","Executable Path":"可执行文件路径","Expiration Date: %{date}":"过期时间: %{date}","Export":"导出","Failed to disable %{msg}":"禁用失败 %{msg}","Failed to enable %{msg}":"启用失败 %{msg}","Failed to get certificate information":"获取证书信息失败","Failed to save, syntax error(s) was detected in the configuration.":"保存失败,在配置中检测到语法错误。","File":"文件","File exists":"文件已存在","File Not Found":"未找到文件","Filter":"过滤","Finished":"完成","Format Code":"代码格式化","Format error %{msg}":"保存错误 %{msg}","Format successfully":"格式化成功","Generate":"生成","Generating private key for registering account":"正在生成私钥用于注册账户","Getting the certificate, please wait...":"正在获取证书,请稍等...","Home":"首页","HTTP Challenge Port":"HTTP Challenge 监听端口","HTTP Port":"HTTP 监听端口","Initial core upgrader error":"初始化核心升级程序错误","Initialing core upgrader":"初始化核心升级器","Inspect Configurations":"检查配置","Install":"安装","Install successfully":"安装成功","Intermediate Certification Authorities: %{issuer}":"中级证书颁发机构: %{issuer}","Issued certificate successfully":"证书申请成功","Jwt Secret":"Jwt 密钥","Last checked at":"最后检查时间","Leave blank for no change":"留空表示不修改","Light":"浅色","Load Averages:":"系统负载:","Location":"Location","Locations":"Locations","Login":"登录","Login successful":"登录成功","Logout successful":"登出成功","Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.":"在获取签发证书前,请确保配置文件中已将 .well-known 目录反向代理到 HTTPChallengePort (默认: 9180)","Manage Configs":"配置管理","Manage Sites":"网站管理","Manage Users":"用户管理","Memory":"内存","Memory and Storage":"内存与存储","Modify":"修改","Modify Config":"修改配置文件","Multi-line Directive":"单行指令","Name":"名称","Network":"网络","Network Statistics":"流量统计","Network Total Receive":"下载流量","Network Total Send":"上传流量","New version released":"新版本发布","Next":"下一步","Nginx Access Log Path":"Nginx 访问日志路径","Nginx Configuration Parse Error":"Nginx 配置解析错误","Nginx Control":"控制 Nginx","Nginx Error Log Path":"Nginx 错误日志路径","Nginx Log":"Nginx 日志","Nginx reloaded successfully":"Nginx 重载成功","Nginx restarted successfully":"Nginx 重启成功","No":"取消","Not Found":"找不到页面","Not Valid Before: %{date}":"此前无效: %{date}","Note":"注意","Obtaining certificate":"正在获取证书","OK":"确定","OS":"OS","OS:":"OS:","Params":"参数","Password":"密码","Password (*)":"密码 (*)","Path":"路径","Perform core upgrade error":"执行核心升级错误","Performing core upgrade":"正在进行核心升级","Please input your E-mail!":"请输入您的邮箱!","Please input your password!":"请输入您的密码!","Please input your username!":"请输入您的用户名!","Preference":"偏好设置","Preparing lego configurations":"正在准备 Lego 的配置","Prohibit changing root password in demo":"禁止在演示模式下修改 root 账户的密码","Prohibit deleting the default user":"禁止删除默认用户","Project Team":"项目团队","Reads":"读","Receive":"下载","Registering user":"正在注册用户","Reinstall":"重新安装","Release Note":"发行日志","Reload":"重载","Reloading":"重载中","Reloading nginx":"正在重载 Nginx","Reset":"重置","Restart":"重启","Restarting":"重启中","Run Mode":"运行模式","Running":"运行中","Save":"保存","Save Directive":"保存指令","Save error %{msg}":"保存错误 %{msg}","Save successfully":"保存成功","Save Successfully":"保存成功","Saved successfully":"保存成功","Selector":"选择器","Send":"上传","Server error":"服务器错误","Server Info":"服务器信息","server_name not found in directives":"未在指令集合中找到 server_name","server_name parameter is required":"必须为 server_name 指令指明参数","Single Directive":"单行指令","Site Logs":"站点列表","Sites List":"站点列表","SSL Certificate Key Path":"SSL证书密钥路径","SSL Certificate Path":"SSL证书路径","SSL Certification Content":"SSL证书内容","SSL Certification Key Content":"SSL证书密钥内容","Status":"状态","Stopped":"已停止","Storage":"存储","Subject Name: %{name}":"主体名称: %{name}","Swap":"Swap","System":"系统","Table":"列表","Terminal":"终端","Terminal Start Command":"终端启动命令","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.":"系统将会每小时检测一次该域名证书,若距离上次签发已超过1个月,则将自动续签。","The filename cannot contain the following characters: %{c}":"文件名不能包含以下字符: %{c}","The server_name in the current configuration must be the domain name you need to get the certificate.":"当前配置中的 server_name 必须为需要申请证书的域名。","The username or password is incorrect":"用户名或密码错误","Theme":"主题","Type":"类型","Updated at":"修改时间","Updated successfully":"更新成功","Upgrade":"升级","Upgraded successfully":"升级成功","Upgrading Nginx UI, please wait...":"正在升级Nginx UI,请等待...","Uptime:":"运行时间:","Username":"用户名","Username (*)":"用户名 (*)","Using HTTP01 challenge provider":"使用 HTTP01 challenge provider","View":"查看","Warning":"警告","Writes":"写","Writing certificate private key to disk":"正在将证书私钥写入磁盘","Writing certificate to disk":"正在将证书写入磁盘","Yes":"是的","You are using the latest version":"您使用的是最新版本","You can check Nginx UI upgrade at this page.":"你可以在这个页面检查Nginx UI的升级。","License":{"Project":"开源许可"}}}
\ No newline at end of file
+{"en":{"About":"About","Action":"Action","Add Directive Below":"Add Directive Below","Add Location":"Add Location","Add Site":"Add Site","Advance Mode":"Advance Mode","Are you sure you want to remove this directive?":"Are you sure you want to remove this directive?","Auto-renewal disabled for %{name}":"Auto-renewal disabled for %{name}","Auto-renewal enabled for %{name}":"Auto-renewal enabled for %{name}","Back":"Back","Base information":"Base information","Basic Mode":"Basic Mode","Build with":"Build with","Cancel":"Cancel","Certificate has expired":"Certificate has expired","Certificate is valid":"Certificate is valid","Certificate Status":"Certificate Status","Comments":"Comments","Configuration Name":"Configuration Name","Configurations":"Configurations","Configure SSL":"Configure SSL","Content":"Content","CPU Status":"CPU Status","CPU:":"CPU:","Create Another":"Create Another","Created at":"Created at","Dashboard":"Dashboard","Database (Optional, default: database)":"Database (Optional, default: database)","Development Mode":"Development Mode","Directive":"Directive","Directives":"Directives","Disable auto-renewal failed for %{name}":"Disable auto-renewal failed for %{name}","Disabled":"Disabled","Disabled successfully":"Disabled successfully","Disk IO":"Disk IO","Domain Config Created Successfully":"Domain Config Created Successfully","Edit %{n}":"Edit %{n}","Edit Configuration":"Edit Configuration","Edit Site":"Edit Site","Email (*)":"Email (*)","Enable auto-renewal failed for %{name}":"Enable auto-renewal failed for %{name}","Enable failed":"Enable failed","Enable TLS":"Enable TLS","Enabled":"Enabled","Enabled successfully":"Enabled successfully","Encrypt website with Let's Encrypt":"Encrypt website with Let's Encrypt","Expiration Date: %{date}":"Expiration Date: %{date}","Failed to disable %{msg}":"Failed to disable %{msg}","Failed to enable %{msg}":"Failed to enable %{msg}","File Not Found":"File Not Found","Finished":"Finished","Getting the certificate, please wait...":"Getting the certificate, please wait...","Home":"Home","Install":"Install","Intermediate Certification Authorities: %{issuer}":"Intermediate Certification Authorities: %{issuer}","Leave blank for no change":"Leave blank for no change","Load Averages:":"Load Averages:","Location":"Location","Locations":"Locations","Login":"Login","Login successful":"Login successful","Logout successful":"Logout successful","Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.":"Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.","Manage Configs":"Manage Configs","Manage Sites":"Manage Sites","Manage Users":"Manage Users","Memory":"Memory","Memory and Storage":"Memory and Storage","Modify Config":"Modify Config","Name":"Name","Network":"Network","Network Statistics":"Network Statistics","Network Total Receive":"Network Total Receive","Network Total Send":"Network Total Send","Next":"Next","No":"No","Not Found":"Not Found","Not Valid Before: %{date}":"Not Valid Before: %{date}","OS:":"OS:","Params":"Params","Password":"Password","Password (*)":"Password (*)","Path":"Path","Please input your E-mail!":"Please input your E-mail!","Please input your password!":"Please input your password!","Please input your username!":"Please input your username!","Project Team":"Project Team","Reads":"Reads","Receive":"Receive","Save":"Save","Save Directive":"Save Directive","Save error %{msg}":"Save error %{msg}","Saved successfully":"Saved successfully","Send":"Send","Server error":"Server error","Server Info":"Server Info","server_name not found in directives":"server_name not found in directives","server_name parameter is required":"server_name parameter is required","Single Directive":"Single Directive","Sites List":"Sites List","Status":"Status","Storage":"Storage","Subject Name: %{name}":"Subject Name: %{name}","Swap":"Swap","Terminal":"Terminal","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.":"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.","The filename cannot contain the following characters: %{c}":"The filename cannot contain the following characters: %{c}","Updated at":"Updated at","Uptime:":"Uptime:","Username":"Username","Username (*)":"Username (*)","Warning":"Warning","Writes":"Writes","Yes":"Yes","License":{"Project":"License"}},"zh_TW":{"About":"關於","Access Logs":"訪問日誌","Action":"操作","Add":"新增","Add Directive Below":"在下面新增指令","Add Location":"新增 Location","Add Site":"新增站點","Advance Mode":"高階模式","Arch":"架搆","Are you sure you want to delete?":"你確定你要刪除?","Are you sure you want to remove this directive?":"您確定要刪除這條指令?","Are you sure you want to remove this location?":"您確定要刪除此 Location 嗎?","Author":"作者","Auto":"自動","Auto Cert":"自動更新","Auto cert is enabled, please do not modify this certification.":"自動證書已啟用,請不要修改此證書。","Auto Refresh":"自動刷新","Auto-Cert Log":"自動證書日誌","Auto-renewal disabled for %{name}":"已關閉 %{name} 自動續簽","Auto-renewal enabled for %{name}":"已啟用 %{name} 自動續簽","Back":"返回","Back Home":"回到首頁","Base information":"基本訊息","Basic Mode":"基本模式","Batch Modify":"批量修改","Build with":"構建基於","Cancel":"取消","Certificate has expired":"此憑證已過期","Certificate is valid":"此憑證有效","Certificate Status":"憑證狀態","Certification":"證照","Change Certificate":"更改證書","Check again":"再次檢查","Comments":"註釋","Config Name":"配置名稱","Config Templates":"配置模板","Configuration file is test successful":"配置文件測試成功","Configuration Name":"配置名稱","Configurations":"配置","Configure SSL":"配置 SSL","Content":"內容","Core Upgrade":"核心升級","CPU Status":"中央處理器狀態","CPU:":"中央處理器:","Create Another":"再創建一個","Created at":"建立時間","Creating client facilitates communication with the CA server":"創建客戶端方便與CA服務器通信","Current Version":"當前版本","Custom":"自訂","Dark":"深色","Dashboard":"儀表盤","Database (Optional, default: database)":"資料庫 (可選,預設: database)","Delete":"刪除","Delete ID: %{id}":"刪除 ID: %{id}","Delete site: %{site_name}":"刪除站點:%{site_name}","Description":"敘述","Development Mode":"開發模式","Dir":"目錄","Directive":"指令","Directives":"指令","Disable auto-renewal failed for %{name}":"關閉 %{name} 自動續簽失敗","Disabled":"禁用","Disabled successfully":"禁用成功","Disk IO":"磁碟 IO","Domain Config Created Successfully":"域名配置文件創建成功","Domains list is empty, try to reopen auto-cert for %{config}":"域列表為空,請嘗試重新打開 %{config} 的自動證書","Download latest release error":"下載最新版本錯誤","Downloading latest release":"正在下載最新版本","Duplicate":"複製","Duplicated successfully":"複製成功","Edit %{n}":"編輯 %{n}","Edit Configuration":"編輯配置","Edit Site":"編輯站點","Email (*)":"郵箱 (*)","Enable auto-renewal failed for %{name}":"啟用 %{name} 自動續簽失敗","Enable failed":"啟用失敗","Enable TLS":"啟用 TLS","Enabled":"啟用","Enabled successfully":"啟用成功","Encrypt website with Let's Encrypt":"用 Let's Encrypt 對網站進行加密","Error":"錯誤","Error Logs":"錯誤日志","Executable Path":"可執行檔路徑","Expiration Date: %{date}":"過期時間: %{date}","Export":"導出","Failed to disable %{msg}":"禁用失敗 %{msg}","Failed to enable %{msg}":"啟用失敗 %{msg}","Failed to get certificate information":"獲取證書信息失敗","Failed to save, syntax error(s) was detected in the configuration.":"保存失敗,在配置中檢測到語法錯誤。","File":"文件","File exists":"文件已存在","File Not Found":"未找到檔案","Filter":"篩選","Finished":"完成","Format Code":"格式化代碼","Format error %{msg}":"格式錯誤 %{msg}","Format successfully":"格式化成功","Generate":"生成","Generating private key for registering account":"生成註冊賬號私鑰","Getting the certificate, please wait...":"正在獲取憑證,請稍等...","Home":"首頁","HTTP Challenge Port":"HTTP 挑戰端口","HTTP Port":"HTTP 監聽埠","Initial core upgrader error":"初始核心升級程序錯誤","Initialing core upgrader":"正在初始化核心升級程序","Inspect Configurations":"檢查配置","Install":"安裝","Install successfully":"安裝成功","Intermediate Certification Authorities: %{issuer}":"中級憑證頒發機構: %{issuer}","Issued certificate successfully":"頒發證書成功","Jwt Secret":"Jwt Secret","Last checked at":"上次檢查時間","Leave blank for no change":"留空表示不修改","Light":"淺色","Load Averages:":"系統負載:","Location":"Location","Locations":"Locations","Login":"登入","Login successful":"登入成功","Logout successful":"登出成功","Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.":"在獲取證書前,請確保配置檔案中已將 .well-known 目錄反向代理到 HTTPChallengePort (預設: 9180)","Manage Configs":"配置管理","Manage Sites":"網站管理","Manage Users":"使用者管理","Memory":"記憶體","Memory and Storage":"記憶體和存儲","Modify":"修改","Modify Config":"修改配置","Multi-line Directive":"多行指令","Name":"名稱","Network":"網路","Network Statistics":"網路統計","Network Total Receive":"下載流量","Network Total Send":"上傳流量","New version released":"新版本發布","Next":"下一步","Nginx Access Log Path":"Nginx 訪問日誌路徑","Nginx Configuration Parse Error":"Nginx 配置解析錯誤","Nginx Control":"Nginx 控件","Nginx Error Log Path":"Nginx 錯誤日誌路徑","Nginx Log":"Nginx 日誌","Nginx reloaded successfully":"Nginx 重載成功","Nginx restarted successfully":"Nginx 重啟成功","No":"取消","Not Found":"找不到頁面","Not Valid Before: %{date}":"此前無效: %{date}","Note":"備註","Obtaining certificate":"正在獲取證書","OK":"確定","OS":"作業系統","OS:":"作業系統:","Params":"參數","Password":"密碼","Password (*)":"密碼 (*)","Path":"路徑","Perform core upgrade error":"執行核心升級錯誤","Performing core upgrade":"正在執行核心升級","Please input name, this will be used as the filename of the new configuration!":"請輸入名稱,這將作為新配置的文件名!","Please input your E-mail!":"請輸入您的郵箱!","Please input your password!":"請輸入您的密碼!","Please input your username!":"請輸入您的使用者名稱!","Preference":"設定","Preparing lego configurations":"準備 Lego 配置","Prohibit changing root password in demo":"禁止在demo中修改root密碼","Prohibit deleting the default user":"禁止刪除默認用戶","Project Team":"專案團隊","Reads":"讀","Receive":"下載","Registering user":"註冊用戶","Reinstall":"安裝","Release Note":"發行公告","Reload":"重新載入","Reloading":"换弹中","Reloading nginx":"重载 Nginx","Reset":"重設","Restart":"重新啟動","Restarting":"重啟","Run Mode":"高階模式","Running":"運行中","Save":"儲存","Save Directive":"儲存指令","Save error %{msg}":"儲存錯誤 %{msg}","Save successfully":"保存成功","Save Successfully":"保存成功","Saved successfully":"儲存成功","Selector":"選擇器","Send":"上傳","Server error":"伺服器錯誤","Server Info":"伺服器資訊","server_name not found in directives":"未在指令集合中找到 server_name","server_name parameter is required":"必須為 server_name 指令指明參數","Single Directive":"單行指令","Site Logs":"網站日誌","Sites List":"站點列表","SSL Certificate Key Path":"SSL 證書密鑰路徑","SSL Certificate Path":"SSL證書路徑","SSL Certification Content":"SSL認證內容","SSL Certification Key Content":"SSL 證書密鑰內容","Status":"狀態","Stopped":"已停止","Storage":"儲存","Subject Name: %{name}":"主體名稱: %{name}","Swap":"交換空間","System":"系統","Table":"表格","Terminal":"終端","Terminal Start Command":"終端啟動命令","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.":"系統將會每小時檢測一次該域名證書,若距離上次簽發已超過1個月,則將自動續簽。
如果您之前沒有證書,請先點選「從 Let's Encrypt 獲取證書」。","The filename cannot contain the following characters: %{c}":"檔名不能包含以下字元: %{c}","The server_name in the current configuration must be the domain name you need to get the certificate.":"注意:當前配置中的 server_name 必須為需要申請證書的域名。","The username or password is incorrect":"用戶名或密碼不正確","Theme":"外觀樣式","This auto-cert item is invalid, please remove it.":"此自動證書項無效,請將其刪除。","Type":"類型","Updated at":"修改時間","Updated successfully":"已成功更新","Upgrade":"升級","Upgraded successfully":"升級成功","Upgrading Nginx UI, please wait...":"正在升級 Nginx UI,請稍候...","Uptime:":"執行時間:","Username":"使用者名稱","Username (*)":"使用者名稱 (*)","Using HTTP01 challenge provider":"使用 HTTP01 挑戰提供者","View":"查看","Warning":"警告","Writes":"寫","Writing certificate private key to disk":"將證書私鑰寫入磁盤","Writing certificate to disk":"將證書寫入磁盤","Yes":"是的","You are using the latest version":"您使用的是最新版本","You can check Nginx UI upgrade at this page.":"您可以在此頁面查看 Nginx UI 升級。","License":{"Project":"開源軟體授權條款"}},"zh_CN":{"About":"关于","Access Logs":"访问日志","Action":"操作","Add":"添加","Add Directive Below":"在下面添加指令","Add Location":"添加 Location","Add Site":"添加站点","Advance Mode":"高级模式","Arch":"架构","Are you sure you want to delete?":"您确定要删除吗?","Are you sure you want to remove this directive?":"您确定要删除这条指令?","Are you sure you want to remove this location?":"您确定要删除这个 Location?","Author":"作者","Auto":"自动","Auto Cert":"自动更新","Auto cert is enabled, please do not modify this certification.":"自动更新已启用,请勿修改此证书配置。","Auto Refresh":"自动刷新","Auto-Cert Log":"证书自动续期日志","Auto-renewal disabled for %{name}":"成功关闭 %{name} 自动续签","Auto-renewal enabled for %{name}":"成功启用 %{name} 自动续签","Back":"返回","Back Home":"返回首页","Base information":"基本信息","Basic Mode":"基本模式","Batch Modify":"批量修改","Build with":"构建基于","Cancel":"取消","Certificate has expired":"此证书已过期","Certificate is valid":"此证书有效","Certificate Status":"证书状态","Certification":"证书","Change Certificate":"更改证书","Check again":"重新检查","Comments":"注释","Config Name":"配置文件名称","Config Templates":"配置","Configuration file is test successful":"配置文件测试成功","Configuration Name":"配置名称","Configurations":"配置","Configure SSL":"配置 SSL","Content":"内容","Core Upgrade":"核心升级","CPU Status":"CPU 状态","CPU:":"CPU:","Create Another":"再创建一个","Created at":"创建时间","Creating client facilitates communication with the CA server":"正在创建客户端用于与 CA 服务器通信","Current Version":"当前版本","Custom":"自定义","Dark":"深色","Dashboard":"仪表盘","Database (Optional, default: database)":"数据库 (可选,默认: database)","Delete":"删除","Delete ID: %{id}":"删除 ID: %{id}","Delete site: %{site_name}":"删除站点: %{site_name}","Description":"描述","Development Mode":"开发模式","Dir":"目录","Directive":"指令","Directives":"指令","Disable auto-renewal failed for %{name}":"关闭 %{name} 自动续签失败","Disabled":"禁用","Disabled successfully":"禁用成功","Disk IO":"磁盘 IO","Domain Config Created Successfully":"域名配置文件创建成功","Domains list is empty, try to reopen auto-cert for %{config}":"域名列表为空,请尝试为%{config}重新打开证书自动续期。","Download latest release error":"下载最新版本错误","Downloading latest release":"下载最新版本","Duplicate":"复制","Duplicated successfully":"复制成功","Edit %{n}":"编辑 %{n}","Edit Configuration":"编辑配置","Edit Site":"编辑站点","Email (*)":"邮箱 (*)","Enable auto-renewal failed for %{name}":"启用 %{name} 自动续签失败","Enable failed":"启用失败","Enable TLS":"启用 TLS","Enabled":"启用","Enabled successfully":"启用成功","Encrypt website with Let's Encrypt":"用 Let's Encrypt 对网站进行加密","Error":"错误","Error Logs":"错误日志","Executable Path":"可执行文件路径","Expiration Date: %{date}":"过期时间: %{date}","Export":"导出","Failed to disable %{msg}":"禁用失败 %{msg}","Failed to enable %{msg}":"启用失败 %{msg}","Failed to get certificate information":"获取证书信息失败","Failed to save, syntax error(s) was detected in the configuration.":"保存失败,在配置中检测到语法错误。","File":"文件","File exists":"文件已存在","File Not Found":"未找到文件","Filter":"过滤","Finished":"完成","Format Code":"代码格式化","Format error %{msg}":"保存错误 %{msg}","Format successfully":"格式化成功","Generate":"生成","Generating private key for registering account":"正在生成私钥用于注册账户","Getting the certificate, please wait...":"正在获取证书,请稍等...","Home":"首页","HTTP Challenge Port":"HTTP Challenge 监听端口","HTTP Port":"HTTP 监听端口","Initial core upgrader error":"初始化核心升级程序错误","Initialing core upgrader":"初始化核心升级器","Inspect Configurations":"检查配置","Install":"安装","Install successfully":"安装成功","Intermediate Certification Authorities: %{issuer}":"中级证书颁发机构: %{issuer}","Issued certificate successfully":"证书申请成功","Jwt Secret":"Jwt 密钥","Last checked at":"最后检查时间","Leave blank for no change":"留空表示不修改","Light":"浅色","Load Averages:":"系统负载:","Location":"Location","Locations":"Locations","Login":"登录","Login successful":"登录成功","Logout successful":"登出成功","Make sure you have configured a reverse proxy for .well-known directory to HTTPChallengePort (default: 9180) before getting the certificate.":"在获取签发证书前,请确保配置文件中已将 .well-known 目录反向代理到 HTTPChallengePort (默认: 9180)","Manage Configs":"配置管理","Manage Sites":"网站管理","Manage Users":"用户管理","Memory":"内存","Memory and Storage":"内存与存储","Modify":"修改","Modify Config":"修改配置文件","Multi-line Directive":"单行指令","Name":"名称","Network":"网络","Network Statistics":"流量统计","Network Total Receive":"下载流量","Network Total Send":"上传流量","New version released":"新版本发布","Next":"下一步","Nginx Access Log Path":"Nginx 访问日志路径","Nginx Configuration Parse Error":"Nginx 配置解析错误","Nginx Control":"控制 Nginx","Nginx Error Log Path":"Nginx 错误日志路径","Nginx Log":"Nginx 日志","Nginx reloaded successfully":"Nginx 重载成功","Nginx restarted successfully":"Nginx 重启成功","No":"取消","Not Found":"找不到页面","Not Valid Before: %{date}":"此前无效: %{date}","Note":"注意","Obtaining certificate":"正在获取证书","OK":"确定","OS":"OS","OS:":"OS:","Params":"参数","Password":"密码","Password (*)":"密码 (*)","Path":"路径","Perform core upgrade error":"执行核心升级错误","Performing core upgrade":"正在进行核心升级","Please input name, this will be used as the filename of the new configuration!":"请输入名称,这将被用作新配置的文件名。","Please input your E-mail!":"请输入您的邮箱!","Please input your password!":"请输入您的密码!","Please input your username!":"请输入您的用户名!","Preference":"偏好设置","Preparing lego configurations":"正在准备 Lego 的配置","Prohibit changing root password in demo":"禁止在演示模式下修改 root 账户的密码","Prohibit deleting the default user":"禁止删除默认用户","Project Team":"项目团队","Reads":"读","Receive":"下载","Registering user":"正在注册用户","Reinstall":"重新安装","Release Note":"发行日志","Reload":"重载","Reloading":"重载中","Reloading nginx":"正在重载 Nginx","Reset":"重置","Restart":"重启","Restarting":"重启中","Run Mode":"运行模式","Running":"运行中","Save":"保存","Save Directive":"保存指令","Save error %{msg}":"保存错误 %{msg}","Save successfully":"保存成功","Save Successfully":"保存成功","Saved successfully":"保存成功","Selector":"选择器","Send":"上传","Server error":"服务器错误","Server Info":"服务器信息","server_name not found in directives":"未在指令集合中找到 server_name","server_name parameter is required":"必须为 server_name 指令指明参数","Single Directive":"单行指令","Site Logs":"站点列表","Sites List":"站点列表","SSL Certificate Key Path":"SSL证书密钥路径","SSL Certificate Path":"SSL证书路径","SSL Certification Content":"SSL证书内容","SSL Certification Key Content":"SSL证书密钥内容","Status":"状态","Stopped":"已停止","Storage":"存储","Subject Name: %{name}":"主体名称: %{name}","Swap":"Swap","System":"系统","Table":"列表","Terminal":"终端","Terminal Start Command":"终端启动命令","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.":"系统将会每小时检测一次该域名证书,若距离上次签发已超过1个月,则将自动续签。","The filename cannot contain the following characters: %{c}":"文件名不能包含以下字符: %{c}","The server_name in the current configuration must be the domain name you need to get the certificate.":"当前配置中的 server_name 必须为需要申请证书的域名。","The username or password is incorrect":"用户名或密码错误","Theme":"主题","This auto-cert item is invalid, please remove it.":"这个证书自动续期项目是无效的,请删除。","Type":"类型","Updated at":"修改时间","Updated successfully":"更新成功","Upgrade":"升级","Upgraded successfully":"升级成功","Upgrading Nginx UI, please wait...":"正在升级Nginx UI,请等待...","Uptime:":"运行时间:","Username":"用户名","Username (*)":"用户名 (*)","Using HTTP01 challenge provider":"使用 HTTP01 challenge provider","View":"查看","Warning":"警告","Writes":"写","Writing certificate private key to disk":"正在将证书私钥写入磁盘","Writing certificate to disk":"正在将证书写入磁盘","Yes":"是的","You are using the latest version":"您使用的是最新版本","You can check Nginx UI upgrade at this page.":"你可以在这个页面检查Nginx UI的升级。","License":{"Project":"开源许可"}}}
\ No newline at end of file
diff --git a/frontend/src/language/zh_CN/app.mo b/frontend/src/language/zh_CN/app.mo
index 1bb3ce404cdc97703d45bfaaebf4b6b999aea1df..a51bbe2c3a3270677e88f64a8c850475bc642e81 100644
GIT binary patch
delta 5549
zcmZYC33OD|9mnyT1r%gYFbE365(pZQC5V6vA}$TMR#b!vAxy|<7Me+dXmD6$vq7X`
z3wuNXgAyc)5TIzqr3e*8#VR06W)i@SO2u>P@$~zf_s4RM@9@j#-uvFW+j}z!Cnl`S
z2_0<}zsPZ%B3F{}mpIoy#<^FTsnxkT3C=aZxp*1Q$A-8Z%V
zfbrCipbt;rwHTM^Tu3JnCY*^)aSwLHPtb?wQ1`WN?cLWF
zTTt(Ujj0zsfQ3Ibv
z^%Hx!bII5mwV8uZ{iGwC>>kD@jPJ(U1-Ymhm*5RJ8=K=E)C508t@JC@4jsd;c-qdl
zWY(HM8@vL$qb7Q*wcmq!WMfhHkH?S(%%ji^C!IS2~p5W)3G;>!49|pwbIv6
zE8U4q-t9x}Lr_})I_gDt*kd{1=pdjzX|ou+-LO(sOz6ZT|eK>FGMY{66rVO_EOM=U!Yd-
zEwUJQ9+`tnXwOFpd!f$vMeW3msE!}9^P|l%sJDC^YC>74g_WRoY6fcMr5OGEms8;J
zxK*f^W;@=EAEO5HF38z;e{f`VQ)mMNk9%gnA@#K5xewp&m_3
zY!srcm%cOV^I^xMy)7_YA->(WY3^>bR}v5TTm0-huX2z
zsCVFd4C&?gm4a-+_p0oKdZuZp0q#KEcrWTxF#wfkcFs;EjQPq
z9?3>je{Xl>`Rl^>XwVAxqXs;P`j{NXx%eXv#3}5rAGcvLw(soSHxzqQPe*lJf||%Q
z)J~M5CQ^Z#z{{vdwYxL>uM;O|(7>lrEBg_7Z2T7SI`pBgOGZt&2X@0W)VnbnY*4r623HD<4V5<*?Q0o$S)x|uyuGwp|Z2Zo_0Hp0LT>Pt~Kt~OsWx0~;o2T@yo4CC<(
z>i!?>{CU(n6`$fQC+RP>Td&T
z=iadTyQp{M6RXE}*GtRuZ%IKjZ5M6e#X!AWsn&i!>Mb9M`kr{q+GnB$Dz$pKxyD?N
z+R;r`KY+Ua5H`YEY|r@aI|{?F>DAuKMxs_;fSTA$j1GkQ_?27zC36clrhT`$7d7x_
zsQb>N`iZ~Bt6zpZ4c7)kIx*Nz+<|)O9xxw6ou7*OEjZg;j(P-D*cU%QUH1!W%i~f#
z6Hx7)&8w}wH>$r|QrUmqFv2eIqu$;e)Cv}$K3>(RFQ$)B9Uiyy^{9z7?%}PhGwOUV
ztM^BJ{Dz`-G#eY^Q&ykVgZb@1Ig|4-FHEKdNAqwhnzn%EfJZ|mv=6}rCYrT$|qbAk?wFBMFA*lXFSv}k8
z(@+CGXI7vl7J8jRa|%09JMp2NIBwQsEbZT;zTy6j>gXps->8?@Q47@8CRx1`Mki(t
z!2z5fW%Wg#A-9Qwp7HyrnV+=!52%-`2_IT@G#GV$q?v`9P!P3}NvMfEZRZ!5tL*$n
z)Q)XIUAHsZ&h!7sF8JIoID#6e&g$n-JM$|x$HYFKT~O_5s0pN_?z_)?6!n%rZuO8^
zike6{Hq_^TEd_O4Y1W`R+G`#%Yf%H8MRoAA)nnNvO{5X(tG5H{x-RB5W*=-t`v9DX
z!!e{Uf?X7D$1rNEQ`5W-ZbLn@;Z~o8n%F#ZnYFLC`Xn^*NO9CU;%bmO=TiM2|zq
zKgd{;Nd7=J>iKUcCkfx4?s4)U;nU@okr#*#t@^?--UC&$SsQb#m?^!SROD~bMK92sr#p7k)wdKCOI96jFR=Lh6Sw8nqe%|%#d<Yd`mW#zxg}n~J%;N^Dj7%k_`5gAI-;Wo=}2xTn~4s6MeA5fYN93n
z6N>|_d|`EZ>#GUxpF2kGCr3%1)*qzsM>2#QApaz_kFI!vKyaVqg
z50FB#hI~SHk;{n=KiNZ;tHLo&KQzNBDsW6EcapUvoAe|1kqbu`3Z1N?kLf-u--^#$
zc{uVtVGj`!8p>IbSLE`o#^<8+(O}12KKX*YL?)8MM8`cI(Vt!U`7F_Y*WIHei8LVpB6Eq4JXQ8uvxMrDgj-@Rz13e(
z=o_9tp*l0MT}=Gl8M*%I;?_rF%0?x17?_(8$P4;%0>MIGAn5bwPAr_9>MJam>?_Ro
z75MWf`ty7lMTPl2GSx>`et|Ffsm%PmtiXgRWs{PICJil`m=nm%DD;o_1&cB>{lQ>X
zQBKa}>YAj{4F~5j5}jq!@uWabj&GdbR}|#Rj3AAEUsfQ;uS2J(tCasuO1eG_y189_hu&CM_N`vQeMtNV0%
zE3Qp_)wJ5}>*{A$hM(O~x8v2wiq(-NFNJq6sXm(=8`C7Rcy{fMt>NeA)ve!L{Y}@h
zn6gRzt-erIWY)tlkXE9hDK7Idyv3s+6=Q;TP#_Yh=#z;b&J@f1I+fK`(Da^}4$}ym?mr3mfYeZm8Y0g3eCO
zoL9GNQ{A#>ew!QJRG0PK7B_&4YqwSYew)ELjs-G>3a_zH`=`Dt}S8
Jr}tSJ^FP_Q+DHHZ
delta 5029
zcmYk;33L_J9mnzek`NLii&-Q@$wCN_g(MiXSTt73>G4_5{@n4aXIC*-`|@*9_HltK6CHP+`0G8o%gbC
z;`uRNU2McE$5v17Al(`}R}gTnw3%w1>k#K$Bh17Y%)?0Ris9HB`E!MQ+=4?f3ddM^
z3Pw;a!&IDydAJdCo%7rYcGO^4ynkXeMpJHvYA_LdUXVTn;wHUZ~>_p=Mw>YJ`(eH=JR|7hwkF
zwW!H^4|V;=$fUYDREI9w@vGQ``@68)n09Q6&9MkIBSTR=eF8PY(bx{B*zpQf2P&}@
zZbx%T_L%r$I}|3gnBY0t3fjLS#$v=G&S2eA!4g01lxOv0B?4el^&
zP&0JE%4f{;s2et*I{qDMMv@c#`{X1t|C;K~ROm+iP>XFM>cXk087V{l+)_T&vCUTA
zhP3GpqNctMdt(ICp_#i6^(cx_GgE@Pehh{lVJqfe2WC*gI&|~R3OnHqWR<#Is42T(
z$77fV%|Ht3Mwxg!<|2J?Mab;B5vUoRg&OEQ?1UAVf`>eIG}3>eMtTjohl`*!%|tS4
z1Z`10E*8(+w45V+K%VURgTz?G1v39})
z)J#l6HN4!8uQV%At9uQqLzSqJ?Ly5|4Qj;iqpm-OOs+eHS~LH_R(k)Vll^CxZuUey
zk`m+`HyZWq7oZmDR@9^V2z7&Vs7KX+nyD+ONAe?Tq%ll`&Pzabunnp{6PtVN6tJTP
z`k)%b&i!0q#K!Xg})hIFQQx
zFJ|W~6+g$(e8G>xD$Kyf%(pJ=h-$C^)qyUk8R&!Rz!21pA4mS&bgN&Dx?vS+KwFSW
zaC=ehoo?g#4VjKmbijiyFBs*Siu0dU2WmeUbENqcYO3d81TMx%T!w0373%w718O9@F%l1;jvqBYwfc+ZWvjo2
z4A64{<~x!Ll2A8HvvLlqp#szm?zQp&)K};*D=$Ya&X-Z`Y_{??WbkgU)qjp!%=M`6
zfp0_gJpUFQ{2R4IHPF_~Ht$4Dc*3;ekStDySb=Gw;nY^Z`$!AsJH7l
z>iRm#{oN&YH1hAw@J#=P%}^)iqdL~z%6*Z}yMb0e)9UA<7S~d9qaELe`f+>L`~=m(
zuh1)CCy?de;4aiucQcEvzSJCH_2W?u&a(0{J6?%eq?=GPb{O>*HK4wTqO$$_B%zL{
zW%K;ilRi{vq@{MkI4e&jt%kH
z(F_bXXQM7$Y2_*_zlFMCt@#SJ8?es%U
z?Eov6V5no}Y;&c#4fSjfqB?xu%HN>YR7{?KeNWW!A?7GlN2VeJ@Z5BE)T1&xVTHNQ
zKf!H5P1O$6c{Nu5p?T7dpGDp1qLr_rX5>0H$GCjobX0u-hW`KW$Br%>U=BmA?(tTh
zW4?gu$RAKszX8?o>*hXGJBQ3u<^|OG|3+Q^os|Q8izVp&k77q(vB{_t)6Fci6KdpL
za0>QEz5n~M5bIHoD*I0V`o3m=REHimAG7+2R(={iyyqGQh4w>uPdtD8#&}2MRoW^7Q6JYGStFh+vWjJ9iaEm+
zPuczua|a$Lb4WD#HPJSKbR+*D%Zc8W>Etlc_KD9=pKEOWh5n~oNOcQR`O{&3^Vxrw
zd_=OzgXG5c1UqxdJLET{C(+i%$32FlNh!INRFQ|tjje*6VV;#%o5ye<`MZ^GW;}&A
z$)CwA(v@r?zaj&Ow)sBpZG4d&BEPe`Rromxl6vwd;(fr!eMDO$vX&H*KaxGw55hVo!e$+U@X~6&iZwpJH^g4a*iA%Nn{0ijeJOQ
ziMD+{p&yr>?9a1u33efQWF+ZGo+Yo7CyBP*WGUHBT9b{W2?;IB;ZPGQ@ZE0S%;OXi
zNIXd=apX3lZ5=ri+H>yd;B#?(1Hser34!4E38w<(wJqC~XC?Lu&QC0f40cROjtG{e
zza1WVanXt|pI={Ioq2otwDw;F$97l}D8HPUT0S)^wtPZXT5xsNkw(GF{Oa)F!p;?e
F{{zQB+%EtC
diff --git a/frontend/src/language/zh_CN/app.po b/frontend/src/language/zh_CN/app.po
index 7f96c5456..ad7a1646d 100644
--- a/frontend/src/language/zh_CN/app.po
+++ b/frontend/src/language/zh_CN/app.po
@@ -20,8 +20,8 @@ msgstr "关于"
msgid "Access Logs"
msgstr "访问日志"
-#: 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/cert/Cert.vue:74 src/views/config/config.ts:36
+#: src/views/domain/DomainList.vue:48 src/views/user/User.vue:43
msgid "Action"
msgstr "操作"
@@ -86,11 +86,11 @@ msgstr "作者"
msgid "Auto"
msgstr "自动"
-#: src/views/cert/Cert.vue:41 src/views/domain/cert/ChangeCert.vue:35
+#: src/views/cert/Cert.vue:37 src/views/domain/cert/ChangeCert.vue:35
msgid "Auto Cert"
msgstr "自动更新"
-#: src/views/cert/Cert.vue:8
+#: src/views/cert/Cert.vue:9
msgid "Auto cert is enabled, please do not modify this certification."
msgstr "自动更新已启用,请勿修改此证书配置。"
@@ -98,6 +98,10 @@ msgstr "自动更新已启用,请勿修改此证书配置。"
msgid "Auto Refresh"
msgstr "自动刷新"
+#: src/views/cert/Cert.vue:27
+msgid "Auto-Cert Log"
+msgstr "证书自动续期日志"
+
#: src/views/domain/cert/IssueCert.vue:71
msgid "Auto-renewal disabled for %{name}"
msgstr "成功关闭 %{name} 自动续签"
@@ -151,7 +155,7 @@ msgstr "此证书已过期"
msgid "Certificate is valid"
msgstr "此证书有效"
-#: src/views/cert/Cert.vue:12 src/views/domain/cert/Cert.vue:35
+#: src/views/cert/Cert.vue:34 src/views/domain/cert/Cert.vue:35
msgid "Certificate Status"
msgstr "证书状态"
@@ -173,10 +177,14 @@ msgstr "重新检查"
#: src/views/domain/ngx_conf/directive/DirectiveEditorItem.vue:34
#: src/views/domain/ngx_conf/LocationEditor.vue:35
#: src/views/domain/ngx_conf/LocationEditor.vue:52
-#: src/views/domain/ngx_conf/NgxConfigEditor.vue:180
+#: src/views/domain/ngx_conf/NgxConfigEditor.vue:181
msgid "Comments"
msgstr "注释"
+#: src/views/cert/Cert.vue:32
+msgid "Config Name"
+msgstr "配置文件名称"
+
#: src/views/domain/ngx_conf/ConfigTemplate.vue:61
msgid "Config Templates"
msgstr "配置"
@@ -254,7 +262,13 @@ msgstr "仪表盘"
msgid "Database (Optional, default: database)"
msgstr "数据库 (可选,默认: database)"
-#: src/components/StdDataDisplay/StdTable.vue:528
+#: src/components/StdDataDisplay/StdTable.vue:31
+#: src/components/StdDataDisplay/StdTable.vue:32
+#: src/components/StdDataDisplay/StdTable.vue:37
+#: src/components/StdDataDisplay/StdTable.vue:50
+#: src/components/StdDataDisplay/StdTable.vue:52
+#: src/components/StdDataDisplay/StdTable.vue:53
+#: src/components/StdDataDisplay/StdTable.vue:57
#: src/views/domain/DomainList.vue:19 src/views/domain/DomainList.vue:20
#: src/views/domain/DomainList.vue:21 src/views/domain/DomainList.vue:28
#: src/views/domain/DomainList.vue:32
@@ -265,7 +279,7 @@ msgstr "删除"
msgid "Delete ID: %{id}"
msgstr "删除 ID: %{id}"
-#: src/views/domain/DomainList.vue:81
+#: src/views/domain/DomainList.vue:82
msgid "Delete site: %{site_name}"
msgstr "删除站点: %{site_name}"
@@ -300,15 +314,15 @@ msgstr "指令"
msgid "Disable auto-renewal failed for %{name}"
msgstr "关闭 %{name} 自动续签失败"
-#: src/views/cert/Cert.vue:51 src/views/domain/cert/ChangeCert.vue:45
+#: src/views/cert/Cert.vue:47 src/views/domain/cert/ChangeCert.vue:45
#: 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:16 src/views/domain/DomainList.vue:35
#: src/views/domain/DomainList.vue:7 src/views/domain/DomainList.vue:8
#: src/views/domain/DomainList.vue:9
msgid "Disabled"
msgstr "禁用"
-#: src/views/domain/DomainEdit.vue:146 src/views/domain/DomainList.vue:69
+#: src/views/domain/DomainEdit.vue:146 src/views/domain/DomainList.vue:70
msgid "Disabled successfully"
msgstr "禁用成功"
@@ -316,14 +330,14 @@ msgstr "禁用成功"
msgid "Disk IO"
msgstr "磁盘 IO"
-#: src/views/cert/Cert.vue:32
-msgid "Domain"
-msgstr "域名"
-
#: src/views/domain/DomainAdd.vue:58
msgid "Domain Config Created Successfully"
msgstr "域名配置文件创建成功"
+#: src/views/cert/Cert.vue:21
+msgid "Domains list is empty, try to reopen auto-cert for %{config}"
+msgstr "域名列表为空,请尝试为%{config}重新打开证书自动续期。"
+
#: src/language/constants.ts:26
msgid "Download latest release error"
msgstr "下载最新版本错误"
@@ -334,9 +348,14 @@ msgstr "下载最新版本"
#: src/views/domain/DomainList.vue:14 src/views/domain/DomainList.vue:15
#: src/views/domain/DomainList.vue:16 src/views/domain/DomainList.vue:23
+#: src/views/domain/SiteDuplicate.vue:2
msgid "Duplicate"
msgstr "复制"
+#: src/views/domain/SiteDuplicate.vue:43
+msgid "Duplicated successfully"
+msgstr "复制成功"
+
#: src/views/domain/DomainEdit.vue:4 src/views/domain/DomainEdit.vue:5
msgid "Edit %{n}"
msgstr "编辑 %{n}"
@@ -365,16 +384,16 @@ msgstr "启用失败"
msgid "Enable TLS"
msgstr "启用 TLS"
-#: src/views/cert/Cert.vue:48 src/views/domain/cert/ChangeCert.vue:42
+#: src/views/cert/Cert.vue:44 src/views/domain/cert/ChangeCert.vue:42
#: src/views/domain/DomainEdit.vue:43 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
+#: src/views/domain/DomainList.vue:19 src/views/domain/DomainList.vue:32
msgid "Enabled"
msgstr "启用"
#: src/views/domain/DomainAdd.vue:47 src/views/domain/DomainEdit.vue:137
-#: src/views/domain/DomainList.vue:59
+#: src/views/domain/DomainList.vue:60
msgid "Enabled successfully"
msgstr "启用成功"
@@ -406,11 +425,11 @@ msgstr "过期时间: %{date}"
msgid "Export"
msgstr "导出"
-#: src/views/domain/DomainEdit.vue:149 src/views/domain/DomainList.vue:73
+#: src/views/domain/DomainEdit.vue:149 src/views/domain/DomainList.vue:74
msgid "Failed to disable %{msg}"
msgstr "禁用失败 %{msg}"
-#: src/views/domain/DomainEdit.vue:140 src/views/domain/DomainList.vue:63
+#: src/views/domain/DomainEdit.vue:140 src/views/domain/DomainList.vue:64
msgid "Failed to enable %{msg}"
msgstr "启用失败 %{msg}"
@@ -598,7 +617,7 @@ msgstr "单行指令"
#: src/views/cert/Cert.vue:16 src/views/config/config.ts:9
#: src/views/domain/cert/ChangeCert.vue:19 src/views/domain/DomainEdit.vue:46
-#: src/views/domain/DomainList.vue:15
+#: src/views/domain/DomainList.vue:16 src/views/domain/SiteDuplicate.vue:5
msgid "Name"
msgstr "名称"
@@ -721,6 +740,12 @@ msgstr "执行核心升级错误"
msgid "Performing core upgrade"
msgstr "正在进行核心升级"
+#: src/views/domain/SiteDuplicate.vue:28
+msgid ""
+"Please input name, this will be used as the filename of the new "
+"configuration!"
+msgstr "请输入名称,这将被用作新配置的文件名。"
+
#: src/views/other/Install.vue:36
msgid "Please input your E-mail!"
msgstr "请输入您的邮箱!"
@@ -865,9 +890,8 @@ msgstr "上传"
#: src/components/StdDataDisplay/StdTable.vue:168
#: src/components/StdDataDisplay/StdTable.vue:343
#: src/components/StdDataDisplay/StdTable.vue:463
-#: src/views/config/ConfigEdit.vue:32 src/views/domain/DomainEdit.vue:87
-#: src/views/domain/DomainList.vue:83 src/views/other/Install.vue:71
-#: src/views/preference/Preference.vue:41
+#: src/views/config/ConfigEdit.vue:32 src/views/domain/DomainList.vue:84
+#: src/views/other/Install.vue:71 src/views/preference/Preference.vue:41
msgid "Server error"
msgstr "服务器错误"
@@ -896,23 +920,23 @@ msgstr "站点列表"
msgid "Sites List"
msgstr "站点列表"
-#: src/views/cert/Cert.vue:65
+#: src/views/cert/Cert.vue:61
msgid "SSL Certificate Key Path"
msgstr "SSL证书密钥路径"
-#: src/views/cert/Cert.vue:58
+#: src/views/cert/Cert.vue:54
msgid "SSL Certificate Path"
msgstr "SSL证书路径"
-#: src/views/cert/Cert.vue:19
+#: src/views/cert/Cert.vue:41
msgid "SSL Certification Content"
msgstr "SSL证书内容"
-#: src/views/cert/Cert.vue:22
+#: src/views/cert/Cert.vue:44
msgid "SSL Certification Key Content"
msgstr "SSL证书密钥内容"
-#: src/views/domain/DomainList.vue:24
+#: src/views/domain/DomainList.vue:25
msgid "Status"
msgstr "状态"
@@ -973,12 +997,16 @@ msgstr "用户名或密码错误"
msgid "Theme"
msgstr "主题"
+#: src/views/cert/Cert.vue:15
+msgid "This auto-cert item is invalid, please remove it."
+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/cert/Cert.vue:68 src/views/config/config.ts:29
+#: src/views/domain/DomainList.vue:42 src/views/user/User.vue:37
msgid "Updated at"
msgstr "修改时间"
@@ -1061,6 +1089,9 @@ msgctxt "Project"
msgid "License"
msgstr "开源许可"
+#~ msgid "Domain"
+#~ msgstr "域名"
+
#~ msgid "Do you want to reload Nginx?"
#~ msgstr "你需要重载 Nginx 吗?"
diff --git a/frontend/src/language/zh_TW/app.mo b/frontend/src/language/zh_TW/app.mo
index 9349a997687ac9b80203cc3b39a1749ca6071ea7..df1952ed556a84d112e6464941b28bb26373726b 100644
GIT binary patch
delta 5512
zcmYk<33OD|9mnw-2@zxyLJR`J0}KXD0t$#kML^Vw0%BdLU||RoG9{U$Sy(hM$YK%!
z(T1=XA)pu~7zM{FMo=gymZG9qu@$HVvbk}r9BmIhwclUfALSi?`P}=KyS|yhJy}Px
zBgebMRydAk(v!@%(79nT&b`=CXPqmJcdj*-VLM!jaaf71@EPPkSIwXHSch$Jzts<6
zEcIjP;Rzgwt-3mw=3K-Lrl1C=qizggTMVNboQorHDR#hp*atsG56_^UOGxma>y8&w
zzZ@^Zp{RC7T78^331jKsWl+!nvak#0Vj?a=+ICN&I)2%%??SD_A=FHdTD>0C@pq_p
zF1Wz)w&!{T#JIUtoXy(yn)A
z)EYoHyc7qZ270rdpMcu3X{hICU_>2;DO`c`P&3+uOv2TmM!FmO;(MrpeTA1{ayRF?
z*tb^a#gg>`qB
zQ|)>I^7gu7)XHtO>xWS*(TM8kJM4i!pa$B73cKa5LQQmZgo0)|7O%#s*c+cj&2&3z
zraO_ryMw5eXh6;2Yt(?xqB@S{Gpy?!QSU-m?2Jjc9EYKv+lTs8L=Icw3)BjHgW9t*
zR=<#q*4A`H4YVg}W>=$Ta4qWo5vX@&qSa@j?tch%|4O^Q3N^tRq}_-+KtVTthMK`Q
z$Yk7)$QWFFFFr~*2z7lhY9($&HGIEapJGl$z2(zU1Ik2AtQfUYi%~OQjxC@6brjeh
zw*mFiyn*BKBUDEo!_|P&&HGSWlaGwi%}2chm8h3>FKWvgP#yh%+LBhDzhZ4sThkfa
zMkw^9pc_+ABOQ*K=}1&VV^Iw}fLiitr~zc5W>kPWUyORmmY`O2J!%54payynwPIhQ
z-htB?(aZ5a3i4vUS7l$+o(@5Ea2x8uyHKBsdr<=ppk@+A4LE`tSQ%;}t55^0G@nIn
z$yQW*d-|~dy7663Xol~jIy{W}m>k72{2tSBKI%uv
zVobsHNK@_rG6(l9UJ%2oF?zKVAL;LR*d2A^3NsBg(xIq#;11NlCYxCpN4*HO=f$WF
zDo`u)I>zBK^Azg&ugrhrVCs?9OhbD;9C_j0o#rfzqy8A`!6(fP=2mlu`3`EpAEVxh
zCe-u)vFk3`@23N5LLPEG;s#Mr2gA%U{slMD>XXc%nU9)r1l7S>)LvIvy$03Zc2q}i
zS^WbXK>c&8x26(D|E^0*fsYEda$W=f6a6OC)DsN?0}Wn2DhO0>J`-Wov4QQnul;U^kC>-W4S$DfIA)-~V(rY!Q4QZ{^|4mZ
zKn)WHp6O6xO3wqSh|#H4mT~{0KYXaa2QJ+x4H!xIzBXc0e`U4YifM&Fk=5
z>JzPA>Kk#JC}8Q8-HmgrIvrz38
zVVpky5ejPfQFAq_p=vW~?nce@eN+RVSp7I^>*`U@owf6A_-yL_PUa=ph5BXqE)K#7
zKS=IJ3KOvB5dRx48`V%L@?yIRR6~31`~lQ}kJ$M}JO3@Jy|buy#T)A1mxl4wN1*;8
zayP2IyrK5{KZg_A%LS;f*hQFt8&EgCYUkfXUVL{1`CV~wyit6eyEKf!F=PqRwfl*V
z-;)wDMHP_MtRM?X1JUuQPfKTuDeon3lY8yF$R>}US1+YJo{T&1T!8W)
zh$f-q&tw|uN^U1xwf}FBlZ0Q%kepX*AxAN@i#4J{CD6E%Gwfr-Rjs)-Xq6|j*yS5z;Y}9
z72A(V?$s9cxKl
zONsx9#dIs5o1NbJDB-iK>F3~ZV^d=j~3nY@mpL55>6kZ`UqysrZJ|T0+4P-U>lw3eo
zlAdHa(RYE43rQ0xCRL<}>>%fk>nZ%*DsyqB=0B1`F4;_;BsyO9aZlqrq}J+U#CheH
zC?}m)SMeiqm|RAZ$(!U#(vJ)$ok<31CjR=j6#QSOS1B*B`ZDuHY()~t0J4sZB|1JN
zqsShTPp%{T$!Fv_l1Gjb9TR+7{`TkZV?_VE?tV)WNo(>qQbs)TaQXI*@zLb?Q88_9
z4(1nncjnHFW_0Zl6FWYT6O7JII380zIk9(oP9PL6@UlY%g?a}Mf*g
zRhOocn)+oGO|@kWl{*^NJl9mSJbF4QJ|?dIi4~16Y>%GpUlvooEV=in`l_n>(lt$+
zUT9oY*SMwb^v>l?TbI?ZUDLFsy18UU!|H7fW#!GQm;EwzukooR=Qm!yB{{u))8>+<
zXLmJ}K2iTzRrEyi{?@5}KQ!08a9#bjl2f~$rURO5Ub>=j*Gsg~u)5^bo3C-(>79>6
wtI|5grZ&`WKEL7SjY}JsY-*?|wJ~igsbBh9eQ8bey3HpSE{eW*O-0QA0XH$bTmS$7
delta 5022
zcmYk<4_sE&9mnzWAfiZ$At+SJGYk>sj}3J-O+$^8oK}
zHqEXt_FCd1pLJ|!$aQ3B2j_AE&Q0#3TIc$9bFMw6V+>|v6b`|5I0E@|dF-yjLhOh$
ztUMPZDVJdo7h^VV#4P7LcZ>ry7!mJZ7>&`CyPz6O#9^3<-@$1Z#HFZ)H>0lKid}Ih
zcESeK{f$;WX`V;j_c=!KeD@Uxaa05noa=${NR`XL&X|ijKOQv$Q&A%Oc)9
zVJ)hIhphf1)GIoRy8k?CW-ekHevO_+lE$#=NM#_*sw}PEI_L7xf
zMcQ-+QB&W7BQTQb(9GS2dKF_)GcyTw{|pSj!X)NjCl*k_I&@3SYP;YSWR*is
zW}-$=g1TWoYOO4{@;cP@+fmmy*!jb#5uQS|8^Hq7bzM;dNJaYQxoi%@&t)SZo)}
zLe0d(sD_`m^HpXwYIUzhb*Kh4vK^?I+Kn3Ve$@R(k;!$(QETQiOw#8+x|jd%dYi*h
zuVfN(jVnUE`*PGG-HLiu@1q`Y67{OuP&4&U)GPT4HPRTSLDwaqI+%j0Psdn~gB%Xj
zz(`aBV^AHq6E%{_RzDrJxaOdyaw%$t)}T82I%=fHQ18AOwIqyM<3N!4
zU(Ue^D!z|Je8Er04Va1@m~Y*fiE1zh)qxvPGcXd>fxA%;z7P3x^R0dr>VX?j1KNU2
zg4=~^uPMdz8#qCQdU^)a@bAd#blrOUi!2+1lyfixCm?_BF?RITZNi8Es~^?QJJ`KUEefl>H8s(}|#-v@Q5k?hAP{1A2i6Z5ZD|GAYf
znbAx$1N2;X4x+dr8})7nTR9ii&}h^H3amUG(m5Wdvn}d4LBC9V)P5CcTuWpOE&(5DfeY#Gg
z?*E76`R)=28hJFslJTeqCZjIQMRjbPl?#y0yQx8S}d{s{0EFgO?AFmX!Q@64_W;}RD;W`yw=WdMKv5k4d5{93+PkS7n1AmKPMJ7
zQ{DUX{&nFXDm2oGcEL0&&q9677NSPB7CT|BmES~-^ayH(PNV*@IgcIjON_=qhJQW=
z^_(~}IfMD1Peq1RY&5r_X5uYW14mFDXhJ>cJYI#Lng6%?m`wkC9O`_M*~iR5wR;Py
z-8(!EG-ZY6Jk*U9R$gW0?WhjCZ8oAh(u!)Z4K)*&tUfNwKc9qZFNmEn6V=WTJMWFN
zg96mlPPXz4RL4rpC(QNc8>m;;i0a5iRKx$pAa=?2@6R?zqdGDL+hGZEpXW;L#3O!%
zTWaMM=4#YbZMJe9>VbQ$e89?$R{j`u{aMsMG8ZrwI}GscfvQi#@bCX&9H?ivns=dA
z_q|r0XFiH*cm=8h)u@JR%-2xu>^0vtn^D)dqV8|A@)ww(&;S28(2d=0@GlISX=WB`
zY6sy#9F6?@;bOnbe@bv5>Km;b)y{g<17EfB4pc|$t^TOhH>0P9S~<|-=){+SE=)wt
zKws1ki=n6n3(YC0cUX*i@NCrLTW05LtbP-+99%u>3+n>%UFQ-9@{z=udjHz`kO#>^
z(uFjT5#$&tA-Z`aIYqQp_=Fee7>;LK`4#g@enQ~@(eI1!*E3Ea{^md5(V_5^{uvAX
zGvU2{Ua7}3$S5+1tR+`&Z(89rs<~n0NwR}n-X7;*x)n6T+KynHj4};e<4qkxnw?hhiLoI=iB{iTYur-bW5r3N@~76
z%`ak(e@EUY{mD3Td7Hw)qvUP!Q!<=rOYw2{ViB24t|lAEUF7ms&A~mMl~$QY@lNuI
zm9OLh3cn?9kws((*+hOs?jYKh__)2ek{lvWSlzSuClVrO$Ts5rf!%FHTYIvGU&F4B%nB;$#;V0h0Lk(r8dWGgx0*Lm(Z2M?2z
Date: Wed, 15 Feb 2023 13:00:42 +0800
Subject: [PATCH 6/6] chore: docker development env
---
.air.toml | 4 +-
dev.Dockerfile | 27 +++++
resources/demo/app.ini | 2 +-
resources/development/entrypoint.sh | 13 +++
resources/development/nginx/fastcgi_params | 25 +++++
resources/development/nginx/mime.types | 99 +++++++++++++++++++
resources/development/nginx/modules | 1 +
resources/development/nginx/nginx.conf | 32 ++++++
resources/development/nginx/scgi_params | 17 ++++
.../sites-available/amstourship.jackyu.cn | 28 ++++++
.../nginx/sites-available/homework.jackyu.cn | 84 ++++++++++++++++
.../nginx/sites-available/nginx.jackyu.cn | 27 +++++
.../nginx/sites-available/qi.jackyu.cn | 26 +++++
.../nginx/sites-enabled/amstourship.jackyu.cn | 1 +
.../ssl/amstourship.jackyu.cn/fullchain.cer | 64 ++++++++++++
.../ssl/amstourship.jackyu.cn/private.key | 27 +++++
.../fullchain.cer | 64 ++++++++++++
.../private.key | 27 +++++
resources/development/nginx/uwsgi_params | 17 ++++
resources/development/sources.list | 9 ++
20 files changed, 591 insertions(+), 3 deletions(-)
create mode 100644 dev.Dockerfile
create mode 100644 resources/development/entrypoint.sh
create mode 100644 resources/development/nginx/fastcgi_params
create mode 100644 resources/development/nginx/mime.types
create mode 120000 resources/development/nginx/modules
create mode 100644 resources/development/nginx/nginx.conf
create mode 100644 resources/development/nginx/scgi_params
create mode 100644 resources/development/nginx/sites-available/amstourship.jackyu.cn
create mode 100644 resources/development/nginx/sites-available/homework.jackyu.cn
create mode 100644 resources/development/nginx/sites-available/nginx.jackyu.cn
create mode 100644 resources/development/nginx/sites-available/qi.jackyu.cn
create mode 120000 resources/development/nginx/sites-enabled/amstourship.jackyu.cn
create mode 100644 resources/development/nginx/ssl/amstourship.jackyu.cn/fullchain.cer
create mode 100644 resources/development/nginx/ssl/amstourship.jackyu.cn/private.key
create mode 100644 resources/development/nginx/ssl/amstourship.jackyu.cn_t.jackyu.cn/fullchain.cer
create mode 100644 resources/development/nginx/ssl/amstourship.jackyu.cn_t.jackyu.cn/private.key
create mode 100644 resources/development/nginx/uwsgi_params
create mode 100644 resources/development/sources.list
diff --git a/.air.toml b/.air.toml
index 0d1e7033f..cb229bcce 100644
--- a/.air.toml
+++ b/.air.toml
@@ -7,13 +7,13 @@ tmp_dir = "tmp"
[build]
# Just plain old shell command. You could use `make` as well.
-cmd = "go build -ldflags=\"-X 'github.com/0xJacky/Nginx-UI/server/settings.buildTime=$(date +%s)'\" -o ./tmp/main ."
+cmd = "CGO_ENABLED=1 go build -ldflags=\"-X 'github.com/0xJacky/Nginx-UI/server/settings.buildTime=$(date +%s)'\" -o ./tmp/main ."
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Watch these filename extensions.
-include_ext = ["go", "tpl", "tmpl", "html", "conf"]
+include_ext = ["go", "tpl", "tmpl", "html", "conf", "ini", "toml"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "upload"]
# Watch these directories if you specified.
diff --git a/dev.Dockerfile b/dev.Dockerfile
new file mode 100644
index 000000000..ab9d16a1a
--- /dev/null
+++ b/dev.Dockerfile
@@ -0,0 +1,27 @@
+FROM --platform=linux/amd64 ubuntu:latest
+
+WORKDIR /app
+EXPOSE 80 443
+
+COPY resources/development/sources.list /etc/apt/sources.list
+
+RUN set -x \
+# create nginx user/group first, to be consistent throughout docker variants
+ && addgroup --system --gid 101 nginx \
+ && adduser --system --disabled-login --ingroup nginx --no-create-home --home /nonexistent --gecos "nginx user" --shell /bin/false --uid 101 nginx \
+ && apt update && apt install -y wget nginx gcc curl
+
+RUN wget https://go.dev/dl/go1.20.linux-amd64.tar.gz && \
+ rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.linux-amd64.tar.gz && rm -f go1.20.linux-amd64.tar.gz
+
+ENV PATH="${PATH}:/usr/local/go/bin"
+
+RUN go install github.com/cosmtrek/air@latest
+
+COPY resources/development/entrypoint.sh /entrypoint.sh
+
+RUN chmod a+x /entrypoint.sh \
+ && rm -f /etc/nginx/conf.d/default.conf \
+ && rm -f /usr/etc/nginx/conf.d/default.conf
+
+CMD ["/entrypoint.sh"]
diff --git a/resources/demo/app.ini b/resources/demo/app.ini
index 5f27e8601..b6b737f13 100644
--- a/resources/demo/app.ini
+++ b/resources/demo/app.ini
@@ -1,6 +1,6 @@
[server]
HttpPort = 9000
-RunMode = debug
+RunMode = release
JwtSecret = 2E1CE615-BB15-44F5-B5BE-6B5DA3581D0F
Email = test@jackyu.cn
HTTPChallengePort = 9180
diff --git a/resources/development/entrypoint.sh b/resources/development/entrypoint.sh
new file mode 100644
index 000000000..fa5ebdb36
--- /dev/null
+++ b/resources/development/entrypoint.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+if [ "$(ls -A /etc/nginx)" = "" ]; then
+ echo "Initialing Nginx config dir"
+ cp -rp /usr/etc/nginx/* /etc/nginx/
+ echo "Initialed Nginx config dir"
+fi
+
+echo "export PATH=$PATH:/usr/local/go/bin:$(go env GOPATH)/bin" >> ~/.profile
+source ~/.profile
+
+nginx
+cd /app && air
diff --git a/resources/development/nginx/fastcgi_params b/resources/development/nginx/fastcgi_params
new file mode 100644
index 000000000..28decb955
--- /dev/null
+++ b/resources/development/nginx/fastcgi_params
@@ -0,0 +1,25 @@
+
+fastcgi_param QUERY_STRING $query_string;
+fastcgi_param REQUEST_METHOD $request_method;
+fastcgi_param CONTENT_TYPE $content_type;
+fastcgi_param CONTENT_LENGTH $content_length;
+
+fastcgi_param SCRIPT_NAME $fastcgi_script_name;
+fastcgi_param REQUEST_URI $request_uri;
+fastcgi_param DOCUMENT_URI $document_uri;
+fastcgi_param DOCUMENT_ROOT $document_root;
+fastcgi_param SERVER_PROTOCOL $server_protocol;
+fastcgi_param REQUEST_SCHEME $scheme;
+fastcgi_param HTTPS $https if_not_empty;
+
+fastcgi_param GATEWAY_INTERFACE CGI/1.1;
+fastcgi_param SERVER_SOFTWARE nginx/$nginx_version;
+
+fastcgi_param REMOTE_ADDR $remote_addr;
+fastcgi_param REMOTE_PORT $remote_port;
+fastcgi_param SERVER_ADDR $server_addr;
+fastcgi_param SERVER_PORT $server_port;
+fastcgi_param SERVER_NAME $server_name;
+
+# PHP only, required if PHP was built with --enable-force-cgi-redirect
+fastcgi_param REDIRECT_STATUS 200;
diff --git a/resources/development/nginx/mime.types b/resources/development/nginx/mime.types
new file mode 100644
index 000000000..1c00d701a
--- /dev/null
+++ b/resources/development/nginx/mime.types
@@ -0,0 +1,99 @@
+
+types {
+ text/html html htm shtml;
+ text/css css;
+ text/xml xml;
+ image/gif gif;
+ image/jpeg jpeg jpg;
+ application/javascript js;
+ application/atom+xml atom;
+ application/rss+xml rss;
+
+ text/mathml mml;
+ text/plain txt;
+ text/vnd.sun.j2me.app-descriptor jad;
+ text/vnd.wap.wml wml;
+ text/x-component htc;
+
+ image/avif avif;
+ image/png png;
+ image/svg+xml svg svgz;
+ image/tiff tif tiff;
+ image/vnd.wap.wbmp wbmp;
+ image/webp webp;
+ image/x-icon ico;
+ image/x-jng jng;
+ image/x-ms-bmp bmp;
+
+ font/woff woff;
+ font/woff2 woff2;
+
+ application/java-archive jar war ear;
+ application/json json;
+ application/mac-binhex40 hqx;
+ application/msword doc;
+ application/pdf pdf;
+ application/postscript ps eps ai;
+ application/rtf rtf;
+ application/vnd.apple.mpegurl m3u8;
+ application/vnd.google-earth.kml+xml kml;
+ application/vnd.google-earth.kmz kmz;
+ application/vnd.ms-excel xls;
+ application/vnd.ms-fontobject eot;
+ application/vnd.ms-powerpoint ppt;
+ application/vnd.oasis.opendocument.graphics odg;
+ application/vnd.oasis.opendocument.presentation odp;
+ application/vnd.oasis.opendocument.spreadsheet ods;
+ application/vnd.oasis.opendocument.text odt;
+ application/vnd.openxmlformats-officedocument.presentationml.presentation
+ pptx;
+ application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
+ xlsx;
+ application/vnd.openxmlformats-officedocument.wordprocessingml.document
+ docx;
+ application/vnd.wap.wmlc wmlc;
+ application/wasm wasm;
+ application/x-7z-compressed 7z;
+ application/x-cocoa cco;
+ application/x-java-archive-diff jardiff;
+ application/x-java-jnlp-file jnlp;
+ application/x-makeself run;
+ application/x-perl pl pm;
+ application/x-pilot prc pdb;
+ application/x-rar-compressed rar;
+ application/x-redhat-package-manager rpm;
+ application/x-sea sea;
+ application/x-shockwave-flash swf;
+ application/x-stuffit sit;
+ application/x-tcl tcl tk;
+ application/x-x509-ca-cert der pem crt;
+ application/x-xpinstall xpi;
+ application/xhtml+xml xhtml;
+ application/xspf+xml xspf;
+ application/zip zip;
+
+ application/octet-stream bin exe dll;
+ application/octet-stream deb;
+ application/octet-stream dmg;
+ application/octet-stream iso img;
+ application/octet-stream msi msp msm;
+
+ audio/midi mid midi kar;
+ audio/mpeg mp3;
+ audio/ogg ogg;
+ audio/x-m4a m4a;
+ audio/x-realaudio ra;
+
+ video/3gpp 3gpp 3gp;
+ video/mp2t ts;
+ video/mp4 mp4;
+ video/mpeg mpeg mpg;
+ video/quicktime mov;
+ video/webm webm;
+ video/x-flv flv;
+ video/x-m4v m4v;
+ video/x-mng mng;
+ video/x-ms-asf asx asf;
+ video/x-ms-wmv wmv;
+ video/x-msvideo avi;
+}
diff --git a/resources/development/nginx/modules b/resources/development/nginx/modules
new file mode 120000
index 000000000..4b9b33f10
--- /dev/null
+++ b/resources/development/nginx/modules
@@ -0,0 +1 @@
+/usr/lib/nginx/modules
\ No newline at end of file
diff --git a/resources/development/nginx/nginx.conf b/resources/development/nginx/nginx.conf
new file mode 100644
index 000000000..c59d9d350
--- /dev/null
+++ b/resources/development/nginx/nginx.conf
@@ -0,0 +1,32 @@
+
+user nginx;
+worker_processes auto;
+
+error_log /var/log/nginx/error.log notice;
+pid /var/run/nginx.pid;
+
+
+events {
+ worker_connections 1024;
+}
+
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
+ '$status $body_bytes_sent "$http_referer" '
+ '"$http_user_agent" "$http_x_forwarded_for"';
+
+ access_log /var/log/nginx/access.log main;
+
+ sendfile on;
+ #tcp_nopush on;
+
+ keepalive_timeout 65;
+
+ #gzip on;
+
+ include /etc/nginx/sites-enabled/*;
+}
diff --git a/resources/development/nginx/scgi_params b/resources/development/nginx/scgi_params
new file mode 100644
index 000000000..6d4ce4f3e
--- /dev/null
+++ b/resources/development/nginx/scgi_params
@@ -0,0 +1,17 @@
+
+scgi_param REQUEST_METHOD $request_method;
+scgi_param REQUEST_URI $request_uri;
+scgi_param QUERY_STRING $query_string;
+scgi_param CONTENT_TYPE $content_type;
+
+scgi_param DOCUMENT_URI $document_uri;
+scgi_param DOCUMENT_ROOT $document_root;
+scgi_param SCGI 1;
+scgi_param SERVER_PROTOCOL $server_protocol;
+scgi_param REQUEST_SCHEME $scheme;
+scgi_param HTTPS $https if_not_empty;
+
+scgi_param REMOTE_ADDR $remote_addr;
+scgi_param REMOTE_PORT $remote_port;
+scgi_param SERVER_PORT $server_port;
+scgi_param SERVER_NAME $server_name;
diff --git a/resources/development/nginx/sites-available/amstourship.jackyu.cn b/resources/development/nginx/sites-available/amstourship.jackyu.cn
new file mode 100644
index 000000000..55a3e643f
--- /dev/null
+++ b/resources/development/nginx/sites-available/amstourship.jackyu.cn
@@ -0,0 +1,28 @@
+server {
+ listen 80;
+ listen [::]:80;
+ server_name amstourship.jackyu.cn t.jackyu.cn;
+ root /var/www/amstourship;
+ index index.html;
+ 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:5002;
+ }
+}
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name amstourship.jackyu.cn t.jackyu.cn;
+ ssl_certificate /etc/nginx/ssl/amstourship.jackyu.cn_t.jackyu.cn/fullchain.cer;
+ ssl_certificate_key /etc/nginx/ssl/amstourship.jackyu.cn_t.jackyu.cn/private.key;
+ root /var/www/amstourship;
+ index index.html;
+ 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:5002;
+ }
+}
\ No newline at end of file
diff --git a/resources/development/nginx/sites-available/homework.jackyu.cn b/resources/development/nginx/sites-available/homework.jackyu.cn
new file mode 100644
index 000000000..ee539926b
--- /dev/null
+++ b/resources/development/nginx/sites-available/homework.jackyu.cn
@@ -0,0 +1,84 @@
+server {
+ listen 80;
+ listen [::]:80;
+
+ server_name homework.jackyu.cn;
+ # rewrite ^(.*)$ https://$host$1 permanent;
+ return 307 https://$server_name$request_uri;
+}
+
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+
+ server_name homework.jackyu.cn;
+
+ ssl_certificate /etc/nginx/ssl/jackyu.cn/alpha/jackyu.cn_server_cert.pem;
+ ssl_certificate_key /etc/nginx/ssl/jackyu.cn/alpha/jackyu.cn_key.pem;
+
+ root /var/www/homework/frontend;
+
+ # Add index.php to the list if you are using PHP
+ index index.html;
+
+ location / {
+ # First attempt to serve request as file, then
+ # as directory, then fall back to displaying a 404.
+ index index.html;
+ try_files $uri $uri/ /index.html;
+ }
+
+ location /student {
+ index manage.html;
+ try_files $uri $uri/ /student.html;
+ }
+
+ location /teacher {
+ index manage.html;
+ try_files $uri $uri/ /teacher.html;
+ }
+
+ location /admin {
+ index admin.html;
+ try_files $uri $uri/ /admin.html;
+ }
+
+ location ^~/upload/ {
+ alias /var/www/homework/api/upload/;
+ }
+ include error_json;
+ location /api/ {
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection upgrade;
+
+ proxy_pass http://127.0.0.1:9008/;
+ 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;
+ }
+
+ location /zigbee-pi {
+ alias /var/www/zigbee-pi/frontend/;
+ index index.html;
+ }
+
+ location /zigbee-pi/api/ {
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection upgrade;
+
+ proxy_pass http://127.0.0.1:9200/;
+ 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;
+ }
+}
diff --git a/resources/development/nginx/sites-available/nginx.jackyu.cn b/resources/development/nginx/sites-available/nginx.jackyu.cn
new file mode 100644
index 000000000..254fbbe7b
--- /dev/null
+++ b/resources/development/nginx/sites-available/nginx.jackyu.cn
@@ -0,0 +1,27 @@
+map $http_upgrade $connection_upgrade {
+ default upgrade;
+ '' close;
+}
+server {
+ listen 80;
+ listen [::]:80;
+ server_name nginx.jackyu.cn;
+ rewrite ^(.*)$ https://$host$1 permanent;
+}
+server {
+ listen 443 ssl http2;
+ listen [::]:443 ssl http2;
+ server_name nginx.jackyu.cn;
+ ssl_certificate /etc/nginx/ssl/jackyu.cn/alpha/jackyu.cn_server_cert.pem;
+ ssl_certificate_key /etc/nginx/ssl/jackyu.cn/alpha/jackyu.cn_key.pem;
+ 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/;
+ }
+}
\ No newline at end of file
diff --git a/resources/development/nginx/sites-available/qi.jackyu.cn b/resources/development/nginx/sites-available/qi.jackyu.cn
new file mode 100644
index 000000000..ba93ac6fc
--- /dev/null
+++ b/resources/development/nginx/sites-available/qi.jackyu.cn
@@ -0,0 +1,26 @@
+server {
+ listen 80;
+ listen [::]:80;
+ server_name qi.jackyu.cn;
+ rewrite ^(.*)$ https://$host$1 permanent;
+
+}
+
+server {
+ server_name qi.jackyu.cn;
+ ssl_certificate /etc/nginx/ssl/jackyu.cn/alpha/jackyu.cn_server_cert.pem;
+ ssl_certificate_key /etc/nginx/ssl/jackyu.cn/alpha/jackyu.cn_key.pem;
+ listen 443 ssl;
+ listen [::]:443 ssl;
+
+ location / {
+ proxy_pass http://127.0.0.1:5001/;
+ 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;
+ }
+
+}
+
diff --git a/resources/development/nginx/sites-enabled/amstourship.jackyu.cn b/resources/development/nginx/sites-enabled/amstourship.jackyu.cn
new file mode 120000
index 000000000..e52029a31
--- /dev/null
+++ b/resources/development/nginx/sites-enabled/amstourship.jackyu.cn
@@ -0,0 +1 @@
+/etc/nginx/sites-available/amstourship.jackyu.cn
\ No newline at end of file
diff --git a/resources/development/nginx/ssl/amstourship.jackyu.cn/fullchain.cer b/resources/development/nginx/ssl/amstourship.jackyu.cn/fullchain.cer
new file mode 100644
index 000000000..67d864119
--- /dev/null
+++ b/resources/development/nginx/ssl/amstourship.jackyu.cn/fullchain.cer
@@ -0,0 +1,64 @@
+-----BEGIN CERTIFICATE-----
+MIIFhjCCBG6gAwIBAgITAP/nxy+CumZyaQpnT/Ii3HgcgTANBgkqhkiG9w0BAQsF
+ADBDMQswCQYDVQQGEwJVUzESMBAGA1UEChMJZ29vZCBndXlzMSAwHgYDVQQDExdD
+QSBpbnRlcm1lZGlhdGUgKFJTQSkgQTAeFw0yMzAyMTUwMDUxMDhaFw0yMzA1MTYw
+MDUxMDdaMCAxHjAcBgNVBAMTFWFtc3RvdXJzaGlwLmphY2t5dS5jbjCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAM8xJTQQiLnb9BPViM8eihPlkJD472vV
+g6u5W6NpmGkvqOmKFcC3IY8D77ATa5IlKeVtgomWAtzQDbQ3+Q8p5gqhotQjLzpp
+xjfK+B9zsrILqDGFpw1n8WKGf/QG6/EsOdecO0ntuhwG80OpsQIDMwjchszC4BPc
+vheCAsRV8pflzEqEqbOzeKcPHa6hDO2Bs70k2zP5SuqTl+GjNzeLKaFpJa5F+rrk
++o4uO8pQOvEGaYW/0i9cN3YKmzfp2yuJY63OsbQ8idSXWTCuSayJhnAjYjgkgcRz
+PWiTNsaQgnPATiZqN81hxq63ISb+kLieq+yMkNAO7xVuItOPDIgawRsCAwEAAaOC
+ApQwggKQMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
+BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU2rpC6XfeKM4RRGOOIsXp5MxO
+7yEwKwYDVR0jBCQwIoAgYjWGqKXC1CgUyRtbS1bZxpMqaNdKnoY33nyaZdVEQ/Iw
+cQYIKwYBBQUHAQEEZTBjMCIGCCsGAQUFBzABhhZodHRwOi8vMTI3LjAuMC4xOjQw
+MDIvMD0GCCsGAQUFBzAChjFodHRwOi8vMTI3LjAuMC4xOjQwMDEvYWlhL2lzc3Vl
+ci82NjA1NDQwNDk4MzY5NzQxMCAGA1UdEQQZMBeCFWFtc3RvdXJzaGlwLmphY2t5
+dS5jbjAnBgNVHR8EIDAeMBygGqAYhhZodHRwOi8vZXhhbXBsZS5jb20vY3JsMEAG
+A1UdIAQ5MDcwCAYGZ4EMAQIBMCsGAyoDBDAkMCIGCCsGAQUFBwIBFhZodHRwOi8v
+ZXhhbXBsZS5jb20vY3BzMIIBAwYKKwYBBAHWeQIEAgSB9ASB8QDvAHYAOJiMlNA1
+mMOTLd/pI7q68npCDrlsQeFaqAwasPwEvQMAAAGGUsSzKQAABAMARzBFAiEAslVV
+aGQEFOseeWMIcWStkdY5fpgfc/mU8MNr2l2GjRQCIHu/O4lsj4LBupnVN8jwDBZn
+fta/KSsrQ7LvC0SfE5NJAHUAe90gTyc4KkZpHv2pgeSOS224Md6/21UmWIxRF9mX
+veIAAAGGUsSzKgAABAMARjBEAiAOOoRiMeKGRpQckOp04laE6GxN+h+FKeKRCwZ8
+wrWm3QIgCUFPPuJswg1VumJIsLKGdFPgCG8gJM4jVVFdbO/1+5YwDQYJKoZIhvcN
+AQELBQADggEBAEwax/yM6yfUW/ctPvMeiGh4bSaO9xWbQbKrOw5/+K+upqgXUSh0
+Qo0WzcrLZzT9ecp3K6KTbph58IrPGxWmPZo92W9sm2tAR3NIOtSwjCyfeBou0TCO
+yBeHwiJv2JbEZndUAqaz2cd2PfpPbNOWTS9ag6nWRDPPW42VoSYU0EFwzvGfFswA
+vww+gC4kzHjsZuR7T/0qsnvJ/1IHH8KDfzki0TnZ9PAhUvPFBAgxwDElLqUFkSmA
+hc5BJfFrGInnKe1s+DL5oL6oc9RmDdlLV8v0Ita39GlaOs3rdvt611qON9S1Ea6z
+lkGadI0bxxv5GlN+UhNRxyTKseyAsBdzY64=
+-----END CERTIFICATE-----
+
+-----BEGIN CERTIFICATE-----
+MIIFZzCCA0+gAwIBAgIQHPTBy0utaJ82mHJs9V3u8zANBgkqhkiG9w0BAQsFADA5
+MQswCQYDVQQGEwJVUzESMBAGA1UEChMJZ29vZCBndXlzMRYwFAYDVQQDEw1DQSBy
+b290IChSU0EpMB4XDTIwMDEwMTEyMDAwMFoXDTQwMDEwMTEyMDAwMFowQzELMAkG
+A1UEBhMCVVMxEjAQBgNVBAoTCWdvb2QgZ3V5czEgMB4GA1UEAxMXQ0EgaW50ZXJt
+ZWRpYXRlIChSU0EpIEEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCk
+f1rlGJeno27J8UAltWo8PopRsTP93Il6+L+SScaOUQsM+TCbTO5EJ4xC+d3Unp8v
+iZRLAB1/lGhHh/Uifzov2ux2sa9J4kxlfCxVaaCx6maOs9KnfGUug2hcUCh1oUVv
+zD9X9VkWdBdR+kKJvYqWJlU/EJxEa5ERjFp591LQBpR7ksZrsbLvXeywqVS3ek8s
+d7w+ZqpYOfo6DNLl5aEJlk6F6CiSjmT352n8dnsOEIEL+bOusLhP5F8pED85geU5
+rijc38fZ+gfZAVVenz7kqBh7ld6qT5inIM4uQa7oCuFX2dZ0jqm5TFBBtQp9dkFv
+WFz9kEb/CVJr1IsTdp1PAgMBAAGjggFfMIIBWzAOBgNVHQ8BAf8EBAMCAYYwHQYD
+VR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQAw
+KQYDVR0OBCIEIGI1hqilwtQoFMkbW0tW2caTKmjXSp6GN958mmXVREPyMCsGA1Ud
+IwQkMCKAINmvCHaWHo5MD5lKL52otAsr/TsCX5Hwfy5euQ6zIAWEMFgGCCsGAQUF
+BwEBBEwwSjAjBggrBgEFBQcwAYYXaHR0cDovL2V4YW1wbGUuY29tL29jc3AwIwYI
+KwYBBQUHMAKGF2h0dHA6Ly9leGFtcGxlLmNvbS9yb290MCcGA1UdHwQgMB4wHKAa
+oBiGFmh0dHA6Ly9leGFtcGxlLmNvbS9jcmwwOwYDVR0gBDQwMjAEBgIqAzAqBgIt
+BjAkMCIGCCsGAQUFBwIBFhZodHRwOi8vZXhhbXBsZS5jb20vY3BzMA0GCSqGSIb3
+DQEBCwUAA4ICAQCTLNQlCzHynESAvtPRV1FPaOQhx01RofwS/0Zg3IH5oXxSC98C
+n2L0xHN1gCaJai9XutrFtMCjeBmese48QoPa8MxrB1UpmZ1AuFOQAfHWJZbYPp0V
+PxgY34W9Onb+JPnKTbL9ofKUV0aX67eJ5KKFD1G2z+y9Lz1oA3yJpGzqOY/JCWYz
+q46ik0bmgcGfol6F/T5hoE8pZk8Wr+nNUpSuOSNp7c/g2/pKDRWK8trTrG3owtaJ
+LbQc+W4e97AtTg6DGvR5gftar/+4g2o0xhKSnep+s/bf5NFXVDCTvCmemrbR8Hr7
+NLDKXWuGMoMKIxhyPX6ttpU2Um3rQ1rCQbJ5yWIREZvbdaeK8HSRE3GYE71Z3n/0
+0Kmtg2BKGkrJzcqUSG4o+9mdSjhJ65J76ri5tVQby7Ai7W2KlNjpdI6GYtejUAlf
+vZz5N0e2X36XLPZ8tz04Ix9KLHXMEuA7w/aOglH1Lei+PPp7kBjvXAL66soqCTqu
+49yNbPPGIjGO453+jNzxhbeimh6a5/Fwd4SjsdSBe8AwIGGZTzLiIzCNM5OmcoUf
+Tl5RrVXau4DvX5KvfwOLusl/uJH+7oETJlbi8+fNn2ioYfHg5/Tu3zKZw/Y+6wSA
+LsOIJrFmJEgIBUnWp/B1ZC6TeIokmw5FeJsY1UnFDWsPVcax2T/tg6BZ5Q==
+-----END CERTIFICATE-----
diff --git a/resources/development/nginx/ssl/amstourship.jackyu.cn/private.key b/resources/development/nginx/ssl/amstourship.jackyu.cn/private.key
new file mode 100644
index 000000000..c4388b8a3
--- /dev/null
+++ b/resources/development/nginx/ssl/amstourship.jackyu.cn/private.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEoAIBAAKCAQEAzzElNBCIudv0E9WIzx6KE+WQkPjva9WDq7lbo2mYaS+o6YoV
+wLchjwPvsBNrkiUp5W2CiZYC3NANtDf5DynmCqGi1CMvOmnGN8r4H3OysguoMYWn
+DWfxYoZ/9Abr8Sw515w7Se26HAbzQ6mxAgMzCNyGzMLgE9y+F4ICxFXyl+XMSoSp
+s7N4pw8drqEM7YGzvSTbM/lK6pOX4aM3N4spoWklrkX6uuT6ji47ylA68QZphb/S
+L1w3dgqbN+nbK4ljrc6xtDyJ1JdZMK5JrImGcCNiOCSBxHM9aJM2xpCCc8BOJmo3
+zWHGrrchJv6QuJ6r7IyQ0A7vFW4i048MiBrBGwIDAQABAoIBAFjb3/PLX4gugCh9
+fRYvJ9LOssiqKqyZvsXIUjfsUrRR/2Rhl2C2acsXShW+znS4QZam98QgNQwUorv3
+AXbyZjmLBvVqPZgUwzEseGusyCe5/iF3C6UhPhIeUwQKb+hYKlqBI6BaN2ZLyMrt
+HQAa9SlIwvtQXJ/IHTKhzKP9pHRmETVQCBtJBEl/rxXAZ+Uq+qQde8PJ/UYMTu3f
+Q+r2Rr+pfITJcHBBvB8w03u0hsCpLyN5tJzSKjAUF86lHYEEUh/KM6PrMerYHvsF
+h60VrusUF/jzankkeQFJKsmY043aI3aYjx9+ghj6DOXLhAX52ubB4etsALYpbpTn
+CF6dySkCgYEA66OCWsFl9UrhkAPT+LuKzCs7li+LMCjGjMhXAj0DxVUceG6ZT32H
+rDsEpSPHeL72vAaZIxcnroAJBkz39vYfr49hHUo//GFELiJTlP5S0joZeJvDYhF2
+D1NKzDvLvJq/HkkJor5DGMfTXYbSS8w3tAxacs6o59I1pseOxK333gUCgYEA4Rhf
+73IYw2A44OSBVuy1PxH1zam1AELpUKDHNo1gT9CJhFLiPYuL4Y6jdQl3PVgD23fW
+uTxyu9rC7a/s6HFTx3tIGvUBFKsTLTagHvmZBnx9SKTT//PZELPoObKesNHy3cIS
+NcUoHnRtbvoTIfZXGBOFiByKjz6JrRKNpO4zLJ8Cf1wLgt/wq+MlXPbkh+ihWZZn
+TN9dswEc7HIRz+sP6mkq1fQ1P63NWzHr2+SmFUUVU0wfR7JtWRLQ8LWFW1cczUPG
+viiy2Eu9suRShFOvBKsaBtkV/zxPlc5mutEMiokh6YXrAdiQeaU0aVwfTgZNv3SV
+Xr95+oCTnGGJkjtBuZECgYAXy4vYtCcKAFIC9CT4G23NjQh/BUGtFWW0sEpe6fMU
+fOcMpfZkqyvOXFXLOYTxZyyBNtnsGwfF9ApHNiHFMLW6kL2+m8fRd4Q/c1bZ72jN
+1rsnwsTmedCog1w5PTipWIhkCPAD3yOulIA9CxKatH/ge6/SA7JiEipbsWpn1QZz
+4QKBgCqLsJMIcgZJcJUqor/ta45CppMfM3FXvWyX6tnTK/WyHUHQPN+wfGVB9lNa
+KEHYbF3CfAy2F/RS+aQn8WLCXaurZWsH9spUOPCugj8xtYQ5GFDNrWK9fJclsXFl
+YH0dpcI88pAaKl0ymrjyVp0kFUiaf5JQFKit5UGwGZTtWyvC
+-----END RSA PRIVATE KEY-----
diff --git a/resources/development/nginx/ssl/amstourship.jackyu.cn_t.jackyu.cn/fullchain.cer b/resources/development/nginx/ssl/amstourship.jackyu.cn_t.jackyu.cn/fullchain.cer
new file mode 100644
index 000000000..2428d3aa4
--- /dev/null
+++ b/resources/development/nginx/ssl/amstourship.jackyu.cn_t.jackyu.cn/fullchain.cer
@@ -0,0 +1,64 @@
+-----BEGIN CERTIFICATE-----
+MIIFkzCCBHugAwIBAgITAP9BpIHmYRZFpLiNFNwEvnS6lTANBgkqhkiG9w0BAQsF
+ADBDMQswCQYDVQQGEwJVUzESMBAGA1UEChMJZ29vZCBndXlzMSAwHgYDVQQDExdD
+QSBpbnRlcm1lZGlhdGUgKFJTQSkgQTAeFw0yMzAyMTUwMzM5NDhaFw0yMzA1MTYw
+MzM5NDdaMCAxHjAcBgNVBAMTFWFtc3RvdXJzaGlwLmphY2t5dS5jbjCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAM31HTeIJ6CcbK1Y1MKb1Pylt2/mXvt8
+M4quWNJjJWWelaRCpe/BD/mBCM9RsQUnf5F8m5OW6QAesoM/QiewTAjf06o5WHfX
+SinH9yMKqXMny4nUBI7U5jJEXbiV82HBsuieU5YldeBCMilefjIG3UNfwNqcA40E
+Miq9xZAGQx9lmabno0iyQlltFyYb1l+4CX0SBm5ygBOyyhb4tReZEB1Sn54n5gtZ
+bK4ZvYgPvKg4wHT6f9A+D4GqE1kZPuGqAKkvAmt8whlEUfXL2zlfNOFfuReQNujs
+FrwnJgSVHa0UPhQEB3zowMjmBSqGZnJap9zXS0W8Eu+D5EWFKUVuRy0CAwEAAaOC
+AqEwggKdMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
+BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxKOLPUnFetKJF7xySU+hY/32
+3FwwKwYDVR0jBCQwIoAgYjWGqKXC1CgUyRtbS1bZxpMqaNdKnoY33nyaZdVEQ/Iw
+cQYIKwYBBQUHAQEEZTBjMCIGCCsGAQUFBzABhhZodHRwOi8vMTI3LjAuMC4xOjQw
+MDIvMD0GCCsGAQUFBzAChjFodHRwOi8vMTI3LjAuMC4xOjQwMDEvYWlhL2lzc3Vl
+ci82NjA1NDQwNDk4MzY5NzQxMC0GA1UdEQQmMCSCFWFtc3RvdXJzaGlwLmphY2t5
+dS5jboILdC5qYWNreXUuY24wJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL2V4YW1w
+bGUuY29tL2NybDBABgNVHSAEOTA3MAgGBmeBDAECATArBgMqAwQwJDAiBggrBgEF
+BQcCARYWaHR0cDovL2V4YW1wbGUuY29tL2NwczCCAQMGCisGAQQB1nkCBAIEgfQE
+gfEA7wB2AFLU6MpxhMjJJFwzEHovC5eeKDMFhyNCIngxCrNdv03eAAABhlNfHncA
+AAQDAEcwRQIhAL84cecGwG4bYGHcCxGVwaLPgISazBGaIcOP/11sY78gAiAA+1/3
+XSk0hPTv5zWYwqJIcI0ajGOeiIfaLwpFxnW+DwB1ADiYjJTQNZjDky3f6SO6uvJ6
+Qg65bEHhWqgMGrD8BL0DAAABhlNfHncAAAQDAEYwRAIgS3rH0/r0zBshQN9LwmWv
+JadxbPEJtQuWjhyH/5gln4cCICZlS/B2qYkOZJzQQkjRgnfHrmUc1vRHFNBEGuRR
+HGX2MA0GCSqGSIb3DQEBCwUAA4IBAQBLP7i7PPn3mUtmsYoguW07lQa8abjsHirs
+r5TgfOpWLVFQ8ASWuIu/OTLdKrbfTXseZibLKlPU+Zoz+HF8V3lnCmgXbnlQo/ex
++uEDPkLYyXuWe96nssiVgtUAmkSWQOEwhIz0xtWNgskgRVt2c4CihYbqBB3uXLL1
+TubIFHAizRKcQ/JUDfSnieN4R5tX1MIw/TnUmNxj3KMtF1OHsqGo9Pt2z8oRWGf5
+kx2HtgFyIigkTIlUB5TFmlv5HLAtE4H3cc2NSYZ397WXhil2mTqPTBLRXQJwzQ7C
+5tenpmzazqPOOu23QJaA94a7UeQtowcEDkSMoCe/G31leeEUbRjt
+-----END CERTIFICATE-----
+
+-----BEGIN CERTIFICATE-----
+MIIFZzCCA0+gAwIBAgIQHPTBy0utaJ82mHJs9V3u8zANBgkqhkiG9w0BAQsFADA5
+MQswCQYDVQQGEwJVUzESMBAGA1UEChMJZ29vZCBndXlzMRYwFAYDVQQDEw1DQSBy
+b290IChSU0EpMB4XDTIwMDEwMTEyMDAwMFoXDTQwMDEwMTEyMDAwMFowQzELMAkG
+A1UEBhMCVVMxEjAQBgNVBAoTCWdvb2QgZ3V5czEgMB4GA1UEAxMXQ0EgaW50ZXJt
+ZWRpYXRlIChSU0EpIEEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCk
+f1rlGJeno27J8UAltWo8PopRsTP93Il6+L+SScaOUQsM+TCbTO5EJ4xC+d3Unp8v
+iZRLAB1/lGhHh/Uifzov2ux2sa9J4kxlfCxVaaCx6maOs9KnfGUug2hcUCh1oUVv
+zD9X9VkWdBdR+kKJvYqWJlU/EJxEa5ERjFp591LQBpR7ksZrsbLvXeywqVS3ek8s
+d7w+ZqpYOfo6DNLl5aEJlk6F6CiSjmT352n8dnsOEIEL+bOusLhP5F8pED85geU5
+rijc38fZ+gfZAVVenz7kqBh7ld6qT5inIM4uQa7oCuFX2dZ0jqm5TFBBtQp9dkFv
+WFz9kEb/CVJr1IsTdp1PAgMBAAGjggFfMIIBWzAOBgNVHQ8BAf8EBAMCAYYwHQYD
+VR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMBIGA1UdEwEB/wQIMAYBAf8CAQAw
+KQYDVR0OBCIEIGI1hqilwtQoFMkbW0tW2caTKmjXSp6GN958mmXVREPyMCsGA1Ud
+IwQkMCKAINmvCHaWHo5MD5lKL52otAsr/TsCX5Hwfy5euQ6zIAWEMFgGCCsGAQUF
+BwEBBEwwSjAjBggrBgEFBQcwAYYXaHR0cDovL2V4YW1wbGUuY29tL29jc3AwIwYI
+KwYBBQUHMAKGF2h0dHA6Ly9leGFtcGxlLmNvbS9yb290MCcGA1UdHwQgMB4wHKAa
+oBiGFmh0dHA6Ly9leGFtcGxlLmNvbS9jcmwwOwYDVR0gBDQwMjAEBgIqAzAqBgIt
+BjAkMCIGCCsGAQUFBwIBFhZodHRwOi8vZXhhbXBsZS5jb20vY3BzMA0GCSqGSIb3
+DQEBCwUAA4ICAQCTLNQlCzHynESAvtPRV1FPaOQhx01RofwS/0Zg3IH5oXxSC98C
+n2L0xHN1gCaJai9XutrFtMCjeBmese48QoPa8MxrB1UpmZ1AuFOQAfHWJZbYPp0V
+PxgY34W9Onb+JPnKTbL9ofKUV0aX67eJ5KKFD1G2z+y9Lz1oA3yJpGzqOY/JCWYz
+q46ik0bmgcGfol6F/T5hoE8pZk8Wr+nNUpSuOSNp7c/g2/pKDRWK8trTrG3owtaJ
+LbQc+W4e97AtTg6DGvR5gftar/+4g2o0xhKSnep+s/bf5NFXVDCTvCmemrbR8Hr7
+NLDKXWuGMoMKIxhyPX6ttpU2Um3rQ1rCQbJ5yWIREZvbdaeK8HSRE3GYE71Z3n/0
+0Kmtg2BKGkrJzcqUSG4o+9mdSjhJ65J76ri5tVQby7Ai7W2KlNjpdI6GYtejUAlf
+vZz5N0e2X36XLPZ8tz04Ix9KLHXMEuA7w/aOglH1Lei+PPp7kBjvXAL66soqCTqu
+49yNbPPGIjGO453+jNzxhbeimh6a5/Fwd4SjsdSBe8AwIGGZTzLiIzCNM5OmcoUf
+Tl5RrVXau4DvX5KvfwOLusl/uJH+7oETJlbi8+fNn2ioYfHg5/Tu3zKZw/Y+6wSA
+LsOIJrFmJEgIBUnWp/B1ZC6TeIokmw5FeJsY1UnFDWsPVcax2T/tg6BZ5Q==
+-----END CERTIFICATE-----
diff --git a/resources/development/nginx/ssl/amstourship.jackyu.cn_t.jackyu.cn/private.key b/resources/development/nginx/ssl/amstourship.jackyu.cn_t.jackyu.cn/private.key
new file mode 100644
index 000000000..5273a22e0
--- /dev/null
+++ b/resources/development/nginx/ssl/amstourship.jackyu.cn_t.jackyu.cn/private.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEAzfUdN4gnoJxsrVjUwpvU/KW3b+Ze+3wziq5Y0mMlZZ6VpEKl
+78EP+YEIz1GxBSd/kXybk5bpAB6ygz9CJ7BMCN/TqjlYd9dKKcf3IwqpcyfLidQE
+jtTmMkRduJXzYcGy6J5TliV14EIyKV5+MgbdQ1/A2pwDjQQyKr3FkAZDH2WZpuej
+SLJCWW0XJhvWX7gJfRIGbnKAE7LKFvi1F5kQHVKfnifmC1lsrhm9iA+8qDjAdPp/
+0D4PgaoTWRk+4aoAqS8Ca3zCGURR9cvbOV804V+5F5A26OwWvCcmBJUdrRQ+FAQH
+fOjAyOYFKoZmclqn3NdLRbwS74PkRYUpRW5HLQIDAQABAoIBAGCuuRlxfBDEfave
+cHouxwwXCwanoVzzEAsBD0csLckHaf3jH5xuB/67WRRhp/Tgdt0oHqxpAlYBExHT
+p02UUA02bVmSc/pGAVWdvmEfxy38t2qrMbyPKsTcHRbipY71a/QRJvHsAerViFCt
+QBZh7IqNL64v4ObY2mgAD/ctSWM56gqkFR70X7HpMKQ9Z8Q9iT5IKeFdVtfOcdSq
+1HuAiz4p8v8IOzLHTKruY5OTYM9uyr02FtPoWsoZSyzSjaC8BpO+wnFlVsDXDBNv
+/Kgqyetj6Iqo6cInj8dExpdnCZXqO5j4FXgHABLgjgiIJV+L3F0KY1PbO9NejMEh
+TvKgZiECgYEA6aIGaUtdoPXWY1o4sEjHy8rwQnBs25JPx+4+HOC5wHrsn03yTmXC
+cjKapIySQhasekQZGQk++H4cRyToqR0izIfkqEmOLfec2kKy7FgcSXzs2PKH66om
+4mvYSWVQj0r3SwYyG6UHJJCeY8i+RCLMkEXSkNPvhEQXg7zCI2oMpSUCgYEA4azO
+yfHvUKnJbGW+r3ujbszn7IlsobVTL0Rf4nHLSuKrW0v/OFg9lsjsm6IJyWV1H5KO
+NNNVzpubAESvPujAwiYfSeFLjK67hbEVSaVLp+5ubsGKf+0BDPKh3+yVJ6abHmFo
+lqzqUlZhZteMp8BN+n3fwR/W5RJFAju9o3F972kCgYBq3IlCMA6rSMa+us2jFCcO
+t8wdF38zD0EemYIfg0pzF8aTNvvVkAXYZf0FtqZPRD+vzOYN5YS/9C7K+77PW1xH
+YQDdWIeHzvIXgtqD7+lAU7uhn4075Z/TgLB1IbovUIK79iGFM36I4v0PdwpP7/rR
+Ip6lT8sGHH8E6pmByUfvYQKBgQDhG1r6HZY1w+bjdWoL6SxQ7Zu6Wio9830SfQWX
+/yJlhEyhOOFP9tUYfztk0vEoL0fxQmMPVm8VNCoczmZwPgNoplY3f7+4iOMMrGMr
+nvIkhLUrTWs1x9dwbuYBUyBE+O9qEogdJEZn8zodN41aF3yxDLYREg1tWhfz7ltv
+mVmhYQKBgBFtHgvmlw/fA5P4dfn/xnOFYwz2v+f5dxcD9DXP9Hu53m7mVPTEUC1t
+2SiBaaUMqTiOflW8Aq9qHHvOPNoO1GrGt4oUbhrAzKwxmkiCUyspYTRBOWHbLjW0
+07Mu40Y8I0WqEVenAGIsHfrsXzvdzat0bijsAt+P1LMliRjK7qhh
+-----END RSA PRIVATE KEY-----
diff --git a/resources/development/nginx/uwsgi_params b/resources/development/nginx/uwsgi_params
new file mode 100644
index 000000000..09c732cd6
--- /dev/null
+++ b/resources/development/nginx/uwsgi_params
@@ -0,0 +1,17 @@
+
+uwsgi_param QUERY_STRING $query_string;
+uwsgi_param REQUEST_METHOD $request_method;
+uwsgi_param CONTENT_TYPE $content_type;
+uwsgi_param CONTENT_LENGTH $content_length;
+
+uwsgi_param REQUEST_URI $request_uri;
+uwsgi_param PATH_INFO $document_uri;
+uwsgi_param DOCUMENT_ROOT $document_root;
+uwsgi_param SERVER_PROTOCOL $server_protocol;
+uwsgi_param REQUEST_SCHEME $scheme;
+uwsgi_param HTTPS $https if_not_empty;
+
+uwsgi_param REMOTE_ADDR $remote_addr;
+uwsgi_param REMOTE_PORT $remote_port;
+uwsgi_param SERVER_PORT $server_port;
+uwsgi_param SERVER_NAME $server_name;
diff --git a/resources/development/sources.list b/resources/development/sources.list
new file mode 100644
index 000000000..36daae260
--- /dev/null
+++ b/resources/development/sources.list
@@ -0,0 +1,9 @@
+# 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释
+deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted universe multiverse
+# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted universe multiverse
+deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main restricted universe multiverse
+# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main restricted universe multiverse
+deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports main restricted universe multiverse
+# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports main restricted universe multiverse
+deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-security main restricted universe multiverse
+# deb-src https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-security main restricted universe multiverse