diff --git a/.gitignore b/.gitignore index 41ef1eae..73a3f876 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ database.db tmp node_modules app.ini +dist diff --git a/README.md b/README.md index 7ba3215d..e5679f9b 100644 --- a/README.md +++ b/README.md @@ -74,21 +74,15 @@ server { root /path/to/nginx-ui/frontend/dist; 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 /api/ { - proxy_set_header Host $host; - proxy_set_header X-Real_IP $remote_addr; - proxy_set_header X-Forwarded-For $remote_addr:$remote_port; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; - proxy_pass http://127.0.0.1:9000/; - } + proxy_pass http://127.0.0.1:9000/; + } } ``` diff --git a/server/api/analytic.go b/api/analytic.go similarity index 100% rename from server/api/analytic.go rename to api/analytic.go diff --git a/server/api/api.go b/api/api.go similarity index 100% rename from server/api/api.go rename to api/api.go diff --git a/api/auth.go b/api/auth.go new file mode 100644 index 00000000..6cf7a93a --- /dev/null +++ b/api/auth.go @@ -0,0 +1,57 @@ +package api + +import ( + "github.com/0xJacky/Nginx-UI/model" + "github.com/gin-gonic/gin" + "golang.org/x/crypto/bcrypt" + "net/http" +) + +type LoginUser struct { + Name string `json:"name" binding:"required,max=255"` + Password string `json:"password" binding:"required,max=255"` +} + +func Login(c *gin.Context) { + var user LoginUser + ok := BindAndValid(c, &user) + if !ok { + return + } + + u, _ := model.GetUser(user.Name) + + if err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(user.Password)); err != nil { + c.JSON(http.StatusForbidden, gin.H{ + "message": "用户名或密码错误", + }) + return + } + + token, err := model.GenerateJWT(u.Name) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": err.Error(), + }) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "ok", + "token": token, + }) +} + +func Logout(c *gin.Context) { + token := c.GetHeader("Authorization") + if token != "" { + err := model.DeleteToken(token) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": err.Error(), + }) + return + } + } + c.JSON(http.StatusNoContent, nil) +} diff --git a/server/api/backup.go b/api/backup.go similarity index 72% rename from server/api/backup.go rename to api/backup.go index 806d05d5..0081649e 100644 --- a/server/api/backup.go +++ b/api/backup.go @@ -1,13 +1,13 @@ package api import ( - "github.com/0xJacky/Nginx-UI/server/model" + "github.com/0xJacky/Nginx-UI/model" "github.com/gin-gonic/gin" "github.com/unknwon/com" "net/http" ) -func GetFileBackupList(c *gin.Context) { +func GetFileBackupList(c *gin.Context) { path := c.Query("path") backups := model.GetBackupList(path) @@ -16,7 +16,7 @@ func GetFileBackupList(c *gin.Context) { }) } -func GetFileBackup(c *gin.Context) { +func GetFileBackup(c *gin.Context) { id := c.Param("id") backup := model.GetBackup(com.StrTo(id).MustInt()) diff --git a/server/api/cert.go b/api/cert.go similarity index 98% rename from server/api/cert.go rename to api/cert.go index 62427766..a43ab60f 100644 --- a/server/api/cert.go +++ b/api/cert.go @@ -2,7 +2,7 @@ package api import ( "encoding/json" - "github.com/0xJacky/Nginx-UI/server/tool" + "github.com/0xJacky/Nginx-UI/tool" "github.com/gin-gonic/gin" "github.com/gorilla/websocket" "log" diff --git a/server/api/config.go b/api/config.go similarity index 98% rename from server/api/config.go rename to api/config.go index e01b9eee..80c6e4e1 100644 --- a/server/api/config.go +++ b/api/config.go @@ -1,7 +1,7 @@ package api import ( - "github.com/0xJacky/Nginx-UI/server/tool" + "github.com/0xJacky/Nginx-UI/tool" "github.com/gin-gonic/gin" "io/ioutil" "log" diff --git a/api/domain.go b/api/domain.go new file mode 100644 index 00000000..717ad347 --- /dev/null +++ b/api/domain.go @@ -0,0 +1,263 @@ +package api + +import ( + "github.com/0xJacky/Nginx-UI/model" + "github.com/0xJacky/Nginx-UI/tool" + "github.com/gin-gonic/gin" + "io/ioutil" + "net/http" + "os" + "path/filepath" +) + +func GetDomains(c *gin.Context) { + orderBy := c.Query("order_by") + sort := c.DefaultQuery("sort", "desc") + + mySort := map[string]string{ + "enabled": "bool", + "name": "string", + "modify": "time", + } + + configFiles, err := ioutil.ReadDir(tool.GetNginxConfPath("sites-available")) + + if err != nil { + ErrHandler(c, err) + return + } + + enabledConfig, err := ioutil.ReadDir(filepath.Join(tool.GetNginxConfPath("sites-enabled"))) + + enabledConfigMap := make(map[string]bool) + for i := range enabledConfig { + enabledConfigMap[enabledConfig[i].Name()] = true + } + + if err != nil { + ErrHandler(c, err) + return + } + + var configs []gin.H + + for i := range configFiles { + file := configFiles[i] + if !file.IsDir() { + configs = append(configs, gin.H{ + "name": file.Name(), + "size": file.Size(), + "modify": file.ModTime(), + "enabled": enabledConfigMap[file.Name()], + }) + } + } + + configs = tool.Sort(orderBy, sort, mySort[orderBy], configs) + + c.JSON(http.StatusOK, gin.H{ + "configs": configs, + }) +} + +func GetDomain(c *gin.Context) { + name := c.Param("name") + path := filepath.Join(tool.GetNginxConfPath("sites-available"), name) + + enabled := true + if _, err := os.Stat(filepath.Join(tool.GetNginxConfPath("sites-enabled"), name)); os.IsNotExist(err) { + enabled = false + } + + content, err := ioutil.ReadFile(path) + + if err != nil { + if os.IsNotExist(err) { + c.JSON(http.StatusNotFound, gin.H{ + "message": err.Error(), + }) + return + } + ErrHandler(c, err) + return + } + + _, err = model.FirstCert(name) + + c.JSON(http.StatusOK, gin.H{ + "enabled": enabled, + "name": name, + "config": string(content), + "auto_cert": err == nil, + }) + +} + +func EditDomain(c *gin.Context) { + var err error + name := c.Param("name") + request := make(gin.H) + err = c.BindJSON(&request) + path := filepath.Join(tool.GetNginxConfPath("sites-available"), name) + + err = ioutil.WriteFile(path, []byte(request["content"].(string)), 0644) + if err != nil { + ErrHandler(c, err) + return + } + + enabledConfigFilePath := filepath.Join(tool.GetNginxConfPath("sites-enabled"), name) + if _, err = os.Stat(enabledConfigFilePath); err == nil { + // 测试配置文件 + err = tool.TestNginxConf(enabledConfigFilePath) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": err.Error(), + }) + return + } + + output := tool.ReloadNginx() + + if output != "" { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": output, + }) + return + } + } + + GetDomain(c) +} + +func EnableDomain(c *gin.Context) { + configFilePath := filepath.Join(tool.GetNginxConfPath("sites-available"), c.Param("name")) + enabledConfigFilePath := filepath.Join(tool.GetNginxConfPath("sites-enabled"), c.Param("name")) + + _, err := os.Stat(configFilePath) + + if err != nil { + ErrHandler(c, err) + return + } + + err = os.Symlink(configFilePath, enabledConfigFilePath) + + if err != nil { + ErrHandler(c, err) + return + } + + // 测试配置文件,不通过则撤回启用 + err = tool.TestNginxConf(enabledConfigFilePath) + if err != nil { + _ = os.Remove(enabledConfigFilePath) + c.JSON(http.StatusInternalServerError, gin.H{ + "message": err.Error(), + }) + return + } + + output := tool.ReloadNginx() + + if output != "" { + 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(tool.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 + } + + output := tool.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(tool.GetNginxConfPath("sites-available"), name) + enabledPath := filepath.Join(tool.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 + } + + cert := model.Cert{Domain: name} + _ = cert.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") + cert, err := model.FirstOrCreateCert(domain) + if err != nil { + ErrHandler(c, err) + return + } + c.JSON(http.StatusOK, cert) +} + +func RemoveDomainFromAutoCert(c *gin.Context) { + cert := model.Cert{ + Domain: c.Param("domain"), + } + err := cert.Remove() + + if err != nil { + ErrHandler(c, err) + return + } + c.JSON(http.StatusOK, nil) +} diff --git a/server/api/install.go b/api/install.go similarity index 93% rename from server/api/install.go rename to api/install.go index dc02bdd1..9452f8aa 100644 --- a/server/api/install.go +++ b/api/install.go @@ -1,8 +1,8 @@ package api import ( - "github.com/0xJacky/Nginx-UI/server/model" - "github.com/0xJacky/Nginx-UI/server/settings" + "github.com/0xJacky/Nginx-UI/model" + "github.com/0xJacky/Nginx-UI/settings" "github.com/gin-gonic/gin" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" diff --git a/api/template.go b/api/template.go new file mode 100644 index 00000000..8eaf9de5 --- /dev/null +++ b/api/template.go @@ -0,0 +1,37 @@ +package api + +import ( + "github.com/0xJacky/Nginx-UI/settings" + "github.com/gin-gonic/gin" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" +) + +func GetTemplate(c *gin.Context) { + name := c.Param("name") + path := filepath.Join("template", name) + content, err := ioutil.ReadFile(path) + + _content := string(content) + _content = strings.ReplaceAll(_content, "{{ HTTP01PORT }}", + settings.ServerSettings.HTTPChallengePort) + + if err != nil { + if os.IsNotExist(err) { + c.JSON(http.StatusNotFound, gin.H{ + "message": err.Error(), + }) + return + } + ErrHandler(c, err) + return + } + + c.JSON(http.StatusOK, gin.H{ + "message": "ok", + "template": _content, + }) +} diff --git a/server/api/user.go b/api/user.go similarity index 98% rename from server/api/user.go rename to api/user.go index a3bb3f61..e8885ad4 100644 --- a/server/api/user.go +++ b/api/user.go @@ -2,7 +2,7 @@ package api import ( "errors" - "github.com/0xJacky/Nginx-UI/server/model" + "github.com/0xJacky/Nginx-UI/model" "github.com/gin-gonic/gin" "github.com/spf13/cast" "golang.org/x/crypto/bcrypt" diff --git a/server/app.example.ini b/app.example.ini similarity index 100% rename from server/app.example.ini rename to app.example.ini diff --git a/frontend/dist/favicon.ico b/frontend/dist/favicon.ico deleted file mode 100644 index c6cc998a..00000000 Binary files a/frontend/dist/favicon.ico and /dev/null differ diff --git a/frontend/dist/img/logo.9e691c6b.png b/frontend/dist/img/logo.9e691c6b.png deleted file mode 100644 index 539a114b..00000000 Binary files a/frontend/dist/img/logo.9e691c6b.png and /dev/null differ diff --git a/frontend/dist/img/remixicon.symbol.f09b1c74.svg b/frontend/dist/img/remixicon.symbol.f09b1c74.svg deleted file mode 100644 index 2522b6cf..00000000 --- a/frontend/dist/img/remixicon.symbol.f09b1c74.svg +++ /dev/null @@ -1,11356 +0,0 @@ - - \ No newline at end of file diff --git a/frontend/dist/index.html b/frontend/dist/index.html deleted file mode 100644 index e38fbbf4..00000000 --- a/frontend/dist/index.html +++ /dev/null @@ -1 +0,0 @@ -