Forráskód Böngészése

feat: check nginx config syntax before save

0xJacky 2 éve
szülő
commit
2d30e1e9d7
4 módosított fájl, 436 hozzáadás és 377 törlés
  1. 56 17
      frontend/src/views/domain/DomainEdit.vue
  2. 335 329
      server/api/domain.go
  3. 32 30
      server/api/ngx.go
  4. 13 1
      server/router/middleware.go

+ 56 - 17
frontend/src/views/domain/DomainEdit.vue

@@ -4,11 +4,12 @@ import CodeEditor from '@/components/CodeEditor/CodeEditor.vue'
 
 import NgxConfigEditor from '@/views/domain/ngx_conf/NgxConfigEditor'
 import {useGettext} from 'vue3-gettext'
-import {reactive, ref, watch} from 'vue'
+import {computed, reactive, ref, watch} from 'vue'
 import {useRoute, useRouter} from 'vue-router'
 import domain from '@/api/domain'
 import ngx from '@/api/ngx'
 import {message} from 'ant-design-vue'
+import config from '@/api/config'
 
 
 const {$gettext, interpolate} = useGettext()
@@ -35,17 +36,30 @@ const auto_cert = ref(false)
 const enabled = ref(false)
 const configText = ref('')
 const ok = ref(false)
-const advance_mode = ref(false)
+const advance_mode_ref = ref(false)
 const saving = ref(false)
 const filename = ref('')
+const parse_error_status = ref(false)
+const parse_error_message = ref('')
 
 init()
 
+const advance_mode = computed({
+    get() {
+        return advance_mode_ref.value || parse_error_status.value
+    },
+    set(v) {
+        advance_mode_ref.value = v
+    }
+})
+
 function handle_response(r: any) {
 
     Object.keys(cert_info_map).forEach(v => {
         delete cert_info_map[v]
     })
+    parse_error_status.value = false
+    parse_error_message.value = ''
     filename.value = r.name
     configText.value = r.config
     enabled.value = r.enabled
@@ -58,10 +72,22 @@ function init() {
     if (name.value) {
         domain.get(name.value).then((r: any) => {
             handle_response(r)
-        }).catch(r => {
-            message.error(r.message ?? $gettext('Server error'))
+        }).catch(handle_parse_error)
+    }
+}
+
+function handle_parse_error(r: any) {
+    if (r?.error === 'nginx_config_syntax_error') {
+        parse_error_status.value = true
+        parse_error_message.value = r.message
+        config.get('sites-available/' + name.value).then(r => {
+            configText.value = r.config
         })
+    } else {
+        message.error(r.message ?? $gettext('Server error'))
     }
+
+    throw r
 }
 
 function on_mode_change(advance_mode: boolean) {
@@ -70,17 +96,13 @@ function on_mode_change(advance_mode: boolean) {
     } else {
         return ngx.tokenize_config(configText.value).then((r: any) => {
             Object.assign(ngx_config, r)
-        }).catch((e: any) => {
-            message.error(e?.message ?? $gettext('Server error'))
-        })
+        }).catch(handle_parse_error)
     }
 }
 
 function build_config() {
     return ngx.build_config(ngx_config).then((r: any) => {
         configText.value = r.content
-    }).catch((e: any) => {
-        message.error(e?.message ?? $gettext('Server error'))
     })
 }
 
@@ -88,17 +110,20 @@ const save = async () => {
     saving.value = true
 
     if (!advance_mode.value) {
-        await build_config()
+        try {
+            await build_config()
+        } catch (e) {
+            saving.value = false
+            message.error($gettext('Failed to save, syntax error(s) was detected in the configuration.'))
+            return
+        }
     }
 
-    domain.save(name.value, {name: filename.value, content: configText.value}).then(r => {
+    domain.save(name.value, {name: filename.value || name.value, content: configText.value}).then(r => {
         handle_response(r)
         router.push('/domain/' + filename.value)
         message.success($gettext('Saved successfully'))
-
-    }).catch((e: any) => {
-        message.error(e?.message ?? $gettext('Server error'))
-    }).finally(() => {
+    }).catch(handle_parse_error).finally(() => {
         saving.value = false
     })
 
@@ -145,7 +170,8 @@ function on_change_enabled(checked: boolean) {
             <template #extra>
                 <div class="mode-switch">
                     <div class="switch">
-                        <a-switch size="small" v-model:checked="advance_mode" @change="on_mode_change"/>
+                        <a-switch size="small" :disabled="parse_error_status"
+                                  v-model:checked="advance_mode" @change="on_mode_change"/>
                     </div>
                     <template v-if="advance_mode">
                         <div>{{ $gettext('Advance Mode') }}</div>
@@ -158,7 +184,16 @@ function on_change_enabled(checked: boolean) {
 
             <transition name="slide-fade">
                 <div v-if="advance_mode" key="advance">
-                    <code-editor v-model:content="configText"/>
+                    <div class="parse-error-alert-wrapper" v-if="parse_error_status">
+                        <a-alert :message="$gettext('Nginx Configuration Parse Error')"
+                                 :description="parse_error_message"
+                                 type="error"
+                                 show-icon
+                        />
+                    </div>
+                    <div>
+                        <code-editor v-model:content="configText"/>
+                    </div>
                 </div>
 
                 <div class="domain-edit-container" key="basic" v-else>
@@ -214,6 +249,10 @@ function on_change_enabled(checked: boolean) {
     }
 }
 
+.parse-error-alert-wrapper {
+    margin-bottom: 20px;
+}
+
 .domain-edit-container {
     max-width: 800px;
     margin: 0 auto;

+ 335 - 329
server/api/domain.go

@@ -1,381 +1,387 @@
 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/nginx"
-	"github.com/gin-gonic/gin"
-	"log"
-	"net/http"
-	"os"
-	"path/filepath"
-	"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/nginx"
+    "github.com/gin-gonic/gin"
+    "log"
+    "net/http"
+    "os"
+    "path/filepath"
+    "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.GetNginxConfPath("sites-available"))
-
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-
-	enabledConfig, err := os.ReadDir(filepath.Join(nginx.GetNginxConfPath("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.GetNginxConfPath("sites-available"))
+
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+
+    enabledConfig, err := os.ReadDir(filepath.Join(nginx.GetNginxConfPath("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 := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
+    path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
 
-	enabled := true
-	if _, err := os.Stat(filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)); os.IsNotExist(err) {
-		enabled = false
-	}
+    enabled := true
+    if _, err := os.Stat(filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)); os.IsNotExist(err) {
+        enabled = false
+    }
 
-	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
+    }
 
-	certInfoMap := make(map[int]CertificateInfo)
-	var serverName string
-	for serverIdx, server := range config.Servers {
-		for _, directive := range server.Directives {
+    c.Set("maybe_error", "")
 
-			if directive.Directive == "server_name" {
-				serverName = strings.ReplaceAll(directive.Params, " ", "_")
-				continue
-			}
+    certInfoMap := make(map[int]CertificateInfo)
+    var serverName string
+    for serverIdx, server := range config.Servers {
+        for _, directive := range server.Directives {
 
-			if directive.Directive == "ssl_certificate" {
+            if directive.Directive == "server_name" {
+                serverName = strings.ReplaceAll(directive.Params, " ", "_")
+                continue
+            }
 
-				pubKey, err := cert.GetCertInfo(directive.Params)
+            if directive.Directive == "ssl_certificate" {
 
-				if err != nil {
-					log.Println("Failed to get certificate information", err)
-					break
-				}
+                pubKey, err := cert.GetCertInfo(directive.Params)
 
-				certInfoMap[serverIdx] = CertificateInfo{
-					SubjectName: pubKey.Subject.CommonName,
-					IssuerName:  pubKey.Issuer.CommonName,
-					NotAfter:    pubKey.NotAfter,
-					NotBefore:   pubKey.NotBefore,
-				}
+                if err != nil {
+                    log.Println("Failed to get certificate information", err)
+                    break
+                }
 
-				break
-			}
-		}
-	}
+                certInfoMap[serverIdx] = CertificateInfo{
+                    SubjectName: pubKey.Subject.CommonName,
+                    IssuerName:  pubKey.Issuer.CommonName,
+                    NotAfter:    pubKey.NotAfter,
+                    NotBefore:   pubKey.NotBefore,
+                }
 
-	certModel, _ := model.FirstCert(serverName)
+                break
+            }
+        }
+    }
 
-	c.JSON(http.StatusOK, gin.H{
-		"enabled":   enabled,
-		"name":      name,
-		"config":    config.FmtCode(),
-		"tokenized": config,
-		"auto_cert": certModel.AutoCert == model.AutoCertEnabled,
-		"cert_info": certInfoMap,
-	})
+    certModel, _ := model.FirstCert(serverName)
+
+    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,
+    })
 
 }
 
 func EditDomain(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"`
-	}
-
-	if !BindAndValid(c, &json) {
-		return
-	}
-
-	path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
-
-	err := os.WriteFile(path, []byte(json.Content), 0644)
-	if err != nil {
-		ErrHandler(c, err)
-		return
-	}
-	enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
-	// rename the config file if needed
-	if name != json.Name {
-		newPath := filepath.Join(nginx.GetNginxConfPath("sites-available"), json.Name)
-		// recreate soft link
-		log.Println(enabledConfigFilePath)
-		if _, err = os.Stat(enabledConfigFilePath); err == nil {
-			log.Println(enabledConfigFilePath)
-			_ = os.Remove(enabledConfigFilePath)
-			enabledConfigFilePath = filepath.Join(nginx.GetNginxConfPath("sites-enabled"), json.Name)
-			err = os.Symlink(newPath, enabledConfigFilePath)
-
-			if err != nil {
-				ErrHandler(c, err)
-				return
-			}
-		}
-		err = os.Rename(path, newPath)
-		if err != nil {
-			ErrHandler(c, err)
-			return
-		}
-		name = json.Name
-		c.Set("rewriteConfigFileName", name)
-
-	}
-
-	enabledConfigFilePath = filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
-	if _, err = os.Stat(enabledConfigFilePath); err == nil {
-		// Test nginx configuration
-		err = nginx.TestNginxConf()
-		if err != nil {
-			c.JSON(http.StatusInternalServerError, gin.H{
-				"message": err.Error(),
-			})
-			return
-		}
-
-		output := nginx.ReloadNginx()
-
-		if output != "" && strings.Contains(output, "error") {
-			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"`
+    }
+
+    if !BindAndValid(c, &json) {
+        return
+    }
+
+    path := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
+
+    err := os.WriteFile(path, []byte(json.Content), 0644)
+    if err != nil {
+        ErrHandler(c, err)
+        return
+    }
+    enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
+    // rename the config file if needed
+    if name != json.Name {
+        newPath := filepath.Join(nginx.GetNginxConfPath("sites-available"), json.Name)
+        // recreate soft link
+        log.Println(enabledConfigFilePath)
+        if _, err = os.Stat(enabledConfigFilePath); err == nil {
+            log.Println(enabledConfigFilePath)
+            _ = os.Remove(enabledConfigFilePath)
+            enabledConfigFilePath = filepath.Join(nginx.GetNginxConfPath("sites-enabled"), json.Name)
+            err = os.Symlink(newPath, enabledConfigFilePath)
+
+            if err != nil {
+                ErrHandler(c, err)
+                return
+            }
+        }
+        err = os.Rename(path, newPath)
+        if err != nil {
+            ErrHandler(c, err)
+            return
+        }
+        name = json.Name
+        c.Set("rewriteConfigFileName", name)
+
+    }
+
+    enabledConfigFilePath = filepath.Join(nginx.GetNginxConfPath("sites-enabled"), name)
+    if _, err = os.Stat(enabledConfigFilePath); err == nil {
+        // Test nginx configuration
+        err = nginx.TestNginxConf()
+        if err != nil {
+            c.JSON(http.StatusInternalServerError, gin.H{
+                "message": err.Error(),
+                "error":   "nginx_config_syntax_error",
+            })
+            return
+        }
+
+        output := nginx.ReloadNginx()
+
+        if output != "" && strings.Contains(output, "error") {
+            c.JSON(http.StatusInternalServerError, gin.H{
+                "message": output,
+            })
+            return
+        }
+    }
+
+    GetDomain(c)
 }
 
 func EnableDomain(c *gin.Context) {
-	configFilePath := filepath.Join(nginx.GetNginxConfPath("sites-available"), c.Param("name"))
-	enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("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 rollback.
-	err = nginx.TestNginxConf()
-	if err != nil {
-		_ = os.Remove(enabledConfigFilePath)
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": err.Error(),
-		})
-		return
-	}
-
-	output := nginx.ReloadNginx()
-
-	if output != "" && strings.Contains(output, "error") {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": output,
-		})
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"message": "ok",
-	})
+    configFilePath := filepath.Join(nginx.GetNginxConfPath("sites-available"), c.Param("name"))
+    enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("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 rollback.
+    err = nginx.TestNginxConf()
+    if err != nil {
+        _ = os.Remove(enabledConfigFilePath)
+        c.JSON(http.StatusInternalServerError, gin.H{
+            "message": err.Error(),
+        })
+        return
+    }
+
+    output := nginx.ReloadNginx()
+
+    if output != "" && strings.Contains(output, "error") {
+        c.JSON(http.StatusInternalServerError, gin.H{
+            "message": output,
+        })
+        return
+    }
+
+    c.JSON(http.StatusOK, gin.H{
+        "message": "ok",
+    })
 }
 
 func DisableDomain(c *gin.Context) {
-	enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("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.ReloadNginx()
-
-	if output != "" {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"message": output,
-		})
-		return
-	}
-
-	c.JSON(http.StatusOK, gin.H{
-		"message": "ok",
-	})
+    enabledConfigFilePath := filepath.Join(nginx.GetNginxConfPath("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.ReloadNginx()
+
+    if output != "" {
+        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 := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
-	enabledPath := filepath.Join(nginx.GetNginxConfPath("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 := filepath.Join(nginx.GetNginxConfPath("sites-available"), name)
+    enabledPath := filepath.Join(nginx.GetNginxConfPath("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",
+    })
 
 }
 
 func AddDomainToAutoCert(c *gin.Context) {
-	domain := c.Param("domain")
-	domain = strings.ReplaceAll(domain, " ", "_")
-	certModel, err := model.FirstOrCreateCert(domain)
+    domain := c.Param("domain")
+    domain = strings.ReplaceAll(domain, " ", "_")
+    certModel, err := model.FirstOrCreateCert(domain)
 
-	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)
+    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)
 }

+ 32 - 30
server/api/ngx.go

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

+ 13 - 1
server/router/middleware.go

@@ -7,6 +7,7 @@ import (
 	"github.com/0xJacky/Nginx-UI/server/settings"
 	"github.com/gin-contrib/static"
 	"github.com/gin-gonic/gin"
+	"github.com/spf13/cast"
 	"io/fs"
 	"log"
 	"net/http"
@@ -18,7 +19,18 @@ func recovery() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		defer func() {
 			if err := recover(); err != nil {
-				log.Println(err)
+				errorAction := "panic"
+				if action, ok := c.Get("maybe_error"); ok {
+					errorActionMsg := cast.ToString(action)
+					if errorActionMsg != "" {
+						errorAction = errorActionMsg
+					}
+				}
+				log.Println(err.(error).Error())
+				c.JSON(http.StatusInternalServerError, gin.H{
+					"message": err.(error).Error(),
+					"error":   errorAction,
+				})
 			}
 		}()