embed frontend

This commit is contained in:
0xJacky 2022-02-18 00:01:27 +08:00
parent 882fe8c074
commit d09f484790
86 changed files with 884 additions and 12326 deletions

1
.gitignore vendored
View file

@ -4,3 +4,4 @@ database.db
tmp
node_modules
app.ini
dist

View file

@ -74,16 +74,10 @@ 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 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;

57
api/auth.go Normal file
View file

@ -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)
}

View file

@ -1,7 +1,7 @@
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"

View file

@ -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"

View file

@ -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"

263
api/domain.go Normal file
View file

@ -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)
}

View file

@ -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"

37
api/template.go Normal file
View file

@ -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,
})
}

View file

@ -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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

File diff suppressed because it is too large Load diff

Before

Width:  |  Height:  |  Size: 877 KiB

View file

@ -1 +0,0 @@
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta content="IE=edge" http-equiv="X-UA-Compatible"><meta content="width=device-width,initial-scale=1,user-scalable=0" name="viewport"><link href="/favicon.ico" rel="icon"><title>Nginx UI</title><link href="/js/chunk-00be396e.f6afc813.js" rel="prefetch"><link href="/js/chunk-0393876a.0e2e8183.js" rel="prefetch"><link href="/js/chunk-05148b16.66291bd9.js" rel="prefetch"><link href="/js/chunk-09f0acda.b788b1ae.js" rel="prefetch"><link href="/js/chunk-2881409a.1f853726.js" rel="prefetch"><link href="/js/chunk-2d0cf277.c260d8d5.js" rel="prefetch"><link href="/js/chunk-312c57da.53fa07de.js" rel="prefetch"><link href="/js/chunk-4216c952.f0073bdb.js" rel="prefetch"><link href="/js/chunk-46dcb584.5ee0f4ea.js" rel="prefetch"><link href="/js/chunk-4f82bf3d.8d3be338.js" rel="prefetch"><link href="/js/chunk-5573b71a.92b99af4.js" rel="prefetch"><link href="/js/chunk-5d4d188e.b0ffa164.js" rel="prefetch"><link href="/js/chunk-6a4ca29d.5593b7e1.js" rel="prefetch"><link href="/js/chunk-83d83096.72980dc3.js" rel="prefetch"><link href="/js/chunk-b508de6a.b421b1eb.js" rel="prefetch"><link href="/js/chunk-c8c0a686.85e5c7a1.js" rel="prefetch"><link href="/js/chunk-vendors.731e48fc.js" rel="modulepreload" as="script"><link href="/js/index.62a46eff.js" rel="modulepreload" as="script"></head><body><noscript><strong>We're sorry but Nginx UI doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script type="module" src="/js/chunk-vendors.731e48fc.js"></script><script type="module" src="/js/index.62a46eff.js"></script><script>!function(){var e=document,t=e.createElement("script");if(!("noModule"in t)&&"onbeforeload"in t){var n=!1;e.addEventListener("beforeload",function(e){if(e.target===t)n=!0;else if(!e.target.hasAttribute("nomodule")||!n)return;e.preventDefault()},!0),t.type="module",t.src=".",e.head.appendChild(t),t.remove()}}();</script><script src="/js/chunk-vendors-legacy.731e48fc.js" nomodule></script><script src="/js/index-legacy.ec1dfd27.js" nomodule></script></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-0393876a"],{"0efa":function(t,n,e){"use strict";e.r(n);var a=function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("a-card",{attrs:{title:"配置文件编辑"}},[e("vue-itextarea",{model:{value:t.configText,callback:function(n){t.configText=n},expression:"configText"}}),e("footer-tool-bar",[e("a-space",[e("a-button",{on:{click:function(n){return t.$router.go(-1)}}},[t._v("返回")]),e("a-button",{attrs:{type:"primary"},on:{click:t.save}},[t._v("保存")])],1)],1)],1)},o=[],i=(e("b0c0"),e("9c70")),c=e("a002"),s={name:"DomainEdit",components:{FooterToolBar:i["a"],VueItextarea:c["a"]},data:function(){return{name:this.$route.params.name,configText:""}},watch:{$route:function(){this.init()},config:{handler:function(){this.unparse()},deep:!0}},created:function(){this.init()},methods:{init:function(){var t=this;this.name?this.$api.config.get(this.name).then((function(n){t.configText=n.config})).catch((function(n){console.log(n),t.$message.error("服务器错误")})):this.configText=""},save:function(){var t=this;this.$api.config.save(this.name?this.name:this.config.name,{content:this.configText}).then((function(n){t.configText=n.config,t.$message.success("保存成功")})).catch((function(n){console.log(n),t.$message.error("保存错误")}))}}},r=s,f=(e("8f3d"),e("2877")),u=Object(f["a"])(r,a,o,!1,null,"fe43c41a",null);n["default"]=u.exports},1175:function(t,n,e){var a=e("24fb");n=a(!1),n.push([t.i,".ant-card[data-v-fe43c41a]{margin:10px}@media (max-width:512px){.ant-card[data-v-fe43c41a]{margin:10px 0}}",""]),t.exports=n},"48b1":function(t,n,e){var a=e("1175");a.__esModule&&(a=a.default),"string"===typeof a&&(a=[[t.i,a,""]]),a.locals&&(t.exports=a.locals);var o=e("499e").default;o("77d6a146",a,!0,{sourceMap:!1,shadowMode:!1})},"8f3d":function(t,n,e){"use strict";e("48b1")}}]);

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-0393876a"],{"0efa":function(t,n,e){"use strict";e.r(n);var a=function(){var t=this,n=t.$createElement,e=t._self._c||n;return e("a-card",{attrs:{title:"配置文件编辑"}},[e("vue-itextarea",{model:{value:t.configText,callback:function(n){t.configText=n},expression:"configText"}}),e("footer-tool-bar",[e("a-space",[e("a-button",{on:{click:function(n){return t.$router.go(-1)}}},[t._v("返回")]),e("a-button",{attrs:{type:"primary"},on:{click:t.save}},[t._v("保存")])],1)],1)],1)},o=[],i=(e("b0c0"),e("9c70")),c=e("a002"),s={name:"DomainEdit",components:{FooterToolBar:i["a"],VueItextarea:c["a"]},data:function(){return{name:this.$route.params.name,configText:""}},watch:{$route:function(){this.init()},config:{handler:function(){this.unparse()},deep:!0}},created:function(){this.init()},methods:{init:function(){var t=this;this.name?this.$api.config.get(this.name).then((function(n){t.configText=n.config})).catch((function(n){console.log(n),t.$message.error("服务器错误")})):this.configText=""},save:function(){var t=this;this.$api.config.save(this.name?this.name:this.config.name,{content:this.configText}).then((function(n){t.configText=n.config,t.$message.success("保存成功")})).catch((function(n){console.log(n),t.$message.error("保存错误")}))}}},r=s,f=(e("8f3d"),e("2877")),u=Object(f["a"])(r,a,o,!1,null,"fe43c41a",null);n["default"]=u.exports},1175:function(t,n,e){var a=e("24fb");n=a(!1),n.push([t.i,".ant-card[data-v-fe43c41a]{margin:10px}@media (max-width:512px){.ant-card[data-v-fe43c41a]{margin:10px 0}}",""]),t.exports=n},"48b1":function(t,n,e){var a=e("1175");a.__esModule&&(a=a.default),"string"===typeof a&&(a=[[t.i,a,""]]),a.locals&&(t.exports=a.locals);var o=e("499e").default;o("77d6a146",a,!0,{sourceMap:!1,shadowMode:!1})},"8f3d":function(t,n,e){"use strict";e("48b1")}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-09f0acda"],{"0561":function(t,a,e){var o=e("504c");o.__esModule&&(o=o.default),"string"===typeof o&&(o=[[t.i,o,""]]),o.locals&&(t.exports=o.locals);var i=e("499e").default;i("4267656c",o,!0,{sourceMap:!1,shadowMode:!1})},"1f35":function(t,a,e){"use strict";e.r(a);var o=function(){var t=this,a=t.$createElement,e=t._self._c||a;return e("div",{staticClass:"wrapper"},[e("h1",{staticClass:"title"},[t._v(t._s(t.$route.meta.status_code?t.$route.meta.status_code:404))]),e("p",[t._v(t._s(t.$route.meta.error?t.$route.meta.error:"找不到文件"))])])},i=[],c={name:"Error"},r=c,n=(e("629f"),e("2877")),s=Object(n["a"])(r,o,i,!1,null,"0b28b6c0",null);a["default"]=s.exports},"504c":function(t,a,e){var o=e("24fb");a=o(!1),a.push([t.i,"body[data-v-0b28b6c0],div[data-v-0b28b6c0],h1[data-v-0b28b6c0],html[data-v-0b28b6c0]{padding:0;margin:0}body[data-v-0b28b6c0],html[data-v-0b28b6c0]{color:#444;position:relative;font-family:PingFang SC,Helvetica Neue,Helvetica,Arial,CustomFont,Microsoft YaHei UI,Microsoft YaHei,Hiragino Sans GB,sans-serif;background:#fcfcfc;height:100%}h1[data-v-0b28b6c0]{font-size:8em;font-weight:100}a[data-v-0b28b6c0]{color:#4181b9;text-decoration:none;transition:all .3s ease}a[data-v-0b28b6c0]:active,a[data-v-0b28b6c0]:hover{color:#5bb0ed}.wrapper[data-v-0b28b6c0]{position:absolute;top:0;bottom:0;left:0;right:0;font-size:1em;font-weight:400;width:100%;height:30%;line-height:1;margin:auto;text-align:center}",""]),t.exports=a},"629f":function(t,a,e){"use strict";e("0561")}}]);

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-09f0acda"],{"0561":function(t,a,e){var o=e("504c");o.__esModule&&(o=o.default),"string"===typeof o&&(o=[[t.i,o,""]]),o.locals&&(t.exports=o.locals);var i=e("499e").default;i("4267656c",o,!0,{sourceMap:!1,shadowMode:!1})},"1f35":function(t,a,e){"use strict";e.r(a);var o=function(){var t=this,a=t.$createElement,e=t._self._c||a;return e("div",{staticClass:"wrapper"},[e("h1",{staticClass:"title"},[t._v(t._s(t.$route.meta.status_code?t.$route.meta.status_code:404))]),e("p",[t._v(t._s(t.$route.meta.error?t.$route.meta.error:"找不到文件"))])])},i=[],c={name:"Error"},r=c,n=(e("629f"),e("2877")),s=Object(n["a"])(r,o,i,!1,null,"0b28b6c0",null);a["default"]=s.exports},"504c":function(t,a,e){var o=e("24fb");a=o(!1),a.push([t.i,"body[data-v-0b28b6c0],div[data-v-0b28b6c0],h1[data-v-0b28b6c0],html[data-v-0b28b6c0]{padding:0;margin:0}body[data-v-0b28b6c0],html[data-v-0b28b6c0]{color:#444;position:relative;font-family:PingFang SC,Helvetica Neue,Helvetica,Arial,CustomFont,Microsoft YaHei UI,Microsoft YaHei,Hiragino Sans GB,sans-serif;background:#fcfcfc;height:100%}h1[data-v-0b28b6c0]{font-size:8em;font-weight:100}a[data-v-0b28b6c0]{color:#4181b9;text-decoration:none;transition:all .3s ease}a[data-v-0b28b6c0]:active,a[data-v-0b28b6c0]:hover{color:#5bb0ed}.wrapper[data-v-0b28b6c0]{position:absolute;top:0;bottom:0;left:0;right:0;font-size:1em;font-weight:400;width:100%;height:30%;line-height:1;margin:auto;text-align:center}",""]),t.exports=a},"629f":function(t,a,e){"use strict";e("0561")}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0cf277"],{6304:function(e,n,t){"use strict";t.r(n);var c=function(){var e=this,n=e.$createElement,t=e._self._c||n;return t("router-view")},r=[],u={name:"BaseRouterView"},a=u,o=t("2877"),s=Object(o["a"])(a,c,r,!1,null,"375df1cc",null);n["default"]=s.exports}}]);

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-2d0cf277"],{6304:function(e,n,t){"use strict";t.r(n);var c=function(){var e=this,n=e.$createElement,t=e._self._c||n;return t("router-view")},r=[],u={name:"BaseRouterView"},a=u,o=t("2877"),s=Object(o["a"])(a,c,r,!1,null,"375df1cc",null);n["default"]=s.exports}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-4216c952"],{3177:function(e,t,r){var n=r("6f0f");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var a=r("499e").default;a("4d98c476",n,!0,{sourceMap:!1,shadowMode:!1})},"6f0f":function(e,t,r){var n=r("24fb");t=n(!1),t.push([e.i,".container{display:flex;align-items:center;justify-content:center;height:100%}.container .login-form{max-width:400px;width:80%}.container .login-form .project-title{margin:50px}.container .login-form .project-title h1{font-size:50px;font-weight:100;text-align:center}.container .login-form .footer{padding:30px;text-align:center}",""]),e.exports=t},b2b2:function(e,t,r){"use strict";r("3177")},bf90:function(e,t,r){"use strict";r.r(t);var n=function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"container"},[r("div",{staticClass:"login-form"},[e._m(0),r("a-form",{attrs:{id:"components-form-demo-normal-login",form:e.form},on:{submit:e.handleSubmit}},[r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["name",{rules:[{required:!0,message:"请输入用户名"}]}],expression:"[\n 'name',\n { rules: [{ required: true, message: '请输入用户名' }] },\n ]"}],attrs:{placeholder:"Username"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"user"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["password",{rules:[{required:!0,message:"请输入密码"}]}],expression:"[\n 'password',\n { rules: [{ required: true, message: '请输入密码' }] },\n ]"}],attrs:{type:"password",placeholder:"Password"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"lock"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-button",{attrs:{type:"primary",block:!0,"html-type":"submit",loading:e.loading}},[e._v(" 登录 ")])],1)],1),r("div",{staticClass:"footer"},[e._v(" Copyright © 2020 - "+e._s(e.thisYear)+" 0xJacky ")])],1)])},a=[function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"project-title"},[r("h1",[e._v("Nginx UI")])])}],o=r("1da1"),i=(r("96cf"),r("b0c0"),{name:"Login",data:function(){return{form:{},thisYear:(new Date).getFullYear(),loading:!1}},created:function(){this.form=this.$form.createForm(this)},mounted:function(){var e=this;this.$api.install.get_lock().then((function(t){t.lock||e.$router.push("/install")})),this.$store.state.user.token&&this.$router.push("/")},methods:{login:function(e){var t=this;return this.$api.auth.login(e.name,e.password).then(Object(o["a"])(regeneratorRuntime.mark((function e(){var r;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.$message.success("登录成功",1);case 2:return r=t.$route.query.next?t.$route.query.next:"/",e.next=5,t.$router.push(r);case 5:case"end":return e.stop()}}),e)})))).catch((function(e){var r;console.log(e),t.$message.error(null!==(r=e.message)&&void 0!==r?r:"服务器错误")}))},handleSubmit:function(){var e=Object(o["a"])(regeneratorRuntime.mark((function e(t){var r=this;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return t.preventDefault(),this.loading=!0,e.next=4,this.form.validateFields(function(){var e=Object(o["a"])(regeneratorRuntime.mark((function e(t,n){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(t){e.next=3;break}return e.next=3,r.login(n);case 3:r.loading=!1;case 4:case"end":return e.stop()}}),e)})));return function(t,r){return e.apply(this,arguments)}}());case 4:case"end":return e.stop()}}),e,this)})));function t(t){return e.apply(this,arguments)}return t}()}}),s=i,c=(r("b2b2"),r("2877")),u=Object(c["a"])(s,n,a,!1,null,null,null);t["default"]=u.exports}}]);

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-4216c952"],{3177:function(e,t,r){var n=r("6f0f");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var a=r("499e").default;a("4d98c476",n,!0,{sourceMap:!1,shadowMode:!1})},"6f0f":function(e,t,r){var n=r("24fb");t=n(!1),t.push([e.i,".container{display:flex;align-items:center;justify-content:center;height:100%}.container .login-form{max-width:400px;width:80%}.container .login-form .project-title{margin:50px}.container .login-form .project-title h1{font-size:50px;font-weight:100;text-align:center}.container .login-form .footer{padding:30px;text-align:center}",""]),e.exports=t},b2b2:function(e,t,r){"use strict";r("3177")},bf90:function(e,t,r){"use strict";r.r(t);var n=function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"container"},[r("div",{staticClass:"login-form"},[e._m(0),r("a-form",{attrs:{id:"components-form-demo-normal-login",form:e.form},on:{submit:e.handleSubmit}},[r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["name",{rules:[{required:!0,message:"请输入用户名"}]}],expression:"[\n 'name',\n { rules: [{ required: true, message: '请输入用户名' }] },\n ]"}],attrs:{placeholder:"Username"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"user"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["password",{rules:[{required:!0,message:"请输入密码"}]}],expression:"[\n 'password',\n { rules: [{ required: true, message: '请输入密码' }] },\n ]"}],attrs:{type:"password",placeholder:"Password"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"lock"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-button",{attrs:{type:"primary",block:!0,"html-type":"submit",loading:e.loading}},[e._v(" 登录 ")])],1)],1),r("div",{staticClass:"footer"},[e._v(" Copyright © 2020 - "+e._s(e.thisYear)+" 0xJacky ")])],1)])},a=[function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"project-title"},[r("h1",[e._v("Nginx UI")])])}],o=r("1da1"),i=(r("96cf"),r("b0c0"),{name:"Login",data:function(){return{form:{},thisYear:(new Date).getFullYear(),loading:!1}},created:function(){this.form=this.$form.createForm(this)},mounted:function(){var e=this;this.$api.install.get_lock().then((function(t){t.lock||e.$router.push("/install")})),this.$store.state.user.token&&this.$router.push("/")},methods:{login:function(e){var t=this;return this.$api.auth.login(e.name,e.password).then(Object(o["a"])(regeneratorRuntime.mark((function e(){var r;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return e.next=2,t.$message.success("登录成功",1);case 2:return r=t.$route.query.next?t.$route.query.next:"/",e.next=5,t.$router.push(r);case 5:case"end":return e.stop()}}),e)})))).catch((function(e){var r;console.log(e),t.$message.error(null!==(r=e.message)&&void 0!==r?r:"服务器错误")}))},handleSubmit:function(){var e=Object(o["a"])(regeneratorRuntime.mark((function e(t){var r=this;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return t.preventDefault(),this.loading=!0,e.next=4,this.form.validateFields(function(){var e=Object(o["a"])(regeneratorRuntime.mark((function e(t,n){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:if(t){e.next=3;break}return e.next=3,r.login(n);case 3:r.loading=!1;case 4:case"end":return e.stop()}}),e)})));return function(t,r){return e.apply(this,arguments)}}());case 4:case"end":return e.stop()}}),e,this)})));function t(t){return e.apply(this,arguments)}return t}()}}),s=i,c=(r("b2b2"),r("2877")),u=Object(c["a"])(s,n,a,!1,null,null,null);t["default"]=u.exports}}]);

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-46dcb584"],{"0125":function(e,t,r){var a=r("24fb");t=a(!1),t.push([e.i,".project-title{margin:50px}.project-title h1{font-size:50px;font-weight:100;text-align:center}.login-form{max-width:500px;margin:0 auto}footer{padding:30px;text-align:center}",""]),e.exports=t},"0f6e":function(e,t,r){"use strict";r("76f8")},"76f8":function(e,t,r){var a=r("0125");a.__esModule&&(a=a.default),"string"===typeof a&&(a=[[e.i,a,""]]),a.locals&&(e.exports=a.locals);var n=r("499e").default;n("447bb34c",a,!0,{sourceMap:!1,shadowMode:!1})},c756:function(e,t,r){"use strict";r.r(t);var a=function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"login-form"},[e._m(0),r("a-form",{staticClass:"login-form",attrs:{id:"components-form-demo-normal-login",form:e.form},on:{submit:e.handleSubmit}},[r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["email",{rules:[{type:"email",message:"The input is not valid E-mail!"},{required:!0,message:"Please input your E-mail!"}]}],expression:"[\n 'email',\n { rules: [{\n type: 'email',\n message: 'The input is not valid E-mail!',\n },\n {\n required: true,\n message: 'Please input your E-mail!',\n },] },\n ]"}],attrs:{placeholder:"Email"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"mail"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["username",{rules:[{required:!0,message:"Please input your username!"}]}],expression:"[\n 'username',\n { rules: [{ required: true, message: 'Please input your username!' }] },\n ]"}],attrs:{placeholder:"Username"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"user"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["password",{rules:[{required:!0,message:"Please input your Password!"}]}],expression:"[\n 'password',\n { rules: [{ required: true, message: 'Please input your Password!' }] },\n ]"}],attrs:{type:"password",placeholder:"Password"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"lock"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-button",{attrs:{type:"primary",block:!0,"html-type":"submit",loading:e.loading}},[e._v(" 安装 ")])],1)],1),r("footer",[e._v(" Copyright © 2020 - "+e._s(e.thisYear)+" 0xJacky ")])],1)},n=[function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"project-title"},[r("h1",[e._v("Nginx UI")])])}],i=r("1da1"),o=(r("96cf"),{name:"Login",data:function(){return{form:{},lock:!0,thisYear:(new Date).getFullYear(),loading:!1}},created:function(){this.form=this.$form.createForm(this)},mounted:function(){var e=this;this.$api.install.get_lock().then((function(t){t.lock&&e.$router.push("/login")}))},methods:{handleSubmit:function(){var e=Object(i["a"])(regeneratorRuntime.mark((function e(t){var r=this;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return t.preventDefault(),this.loading=!0,e.next=4,this.form.validateFields(function(){var e=Object(i["a"])(regeneratorRuntime.mark((function e(t,a){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:t||r.$api.install.install_nginx_ui(a).then((function(){r.$router.push("/login")})),r.loading=!1;case 2:case"end":return e.stop()}}),e)})));return function(t,r){return e.apply(this,arguments)}}());case 4:case"end":return e.stop()}}),e,this)})));function t(t){return e.apply(this,arguments)}return t}()}}),s=o,l=(r("0f6e"),r("2877")),u=Object(l["a"])(s,a,n,!1,null,null,null);t["default"]=u.exports}}]);

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-46dcb584"],{"0125":function(e,t,r){var a=r("24fb");t=a(!1),t.push([e.i,".project-title{margin:50px}.project-title h1{font-size:50px;font-weight:100;text-align:center}.login-form{max-width:500px;margin:0 auto}footer{padding:30px;text-align:center}",""]),e.exports=t},"0f6e":function(e,t,r){"use strict";r("76f8")},"76f8":function(e,t,r){var a=r("0125");a.__esModule&&(a=a.default),"string"===typeof a&&(a=[[e.i,a,""]]),a.locals&&(e.exports=a.locals);var n=r("499e").default;n("447bb34c",a,!0,{sourceMap:!1,shadowMode:!1})},c756:function(e,t,r){"use strict";r.r(t);var a=function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"login-form"},[e._m(0),r("a-form",{staticClass:"login-form",attrs:{id:"components-form-demo-normal-login",form:e.form},on:{submit:e.handleSubmit}},[r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["email",{rules:[{type:"email",message:"The input is not valid E-mail!"},{required:!0,message:"Please input your E-mail!"}]}],expression:"[\n 'email',\n { rules: [{\n type: 'email',\n message: 'The input is not valid E-mail!',\n },\n {\n required: true,\n message: 'Please input your E-mail!',\n },] },\n ]"}],attrs:{placeholder:"Email"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"mail"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["username",{rules:[{required:!0,message:"Please input your username!"}]}],expression:"[\n 'username',\n { rules: [{ required: true, message: 'Please input your username!' }] },\n ]"}],attrs:{placeholder:"Username"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"user"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-input",{directives:[{name:"decorator",rawName:"v-decorator",value:["password",{rules:[{required:!0,message:"Please input your Password!"}]}],expression:"[\n 'password',\n { rules: [{ required: true, message: 'Please input your Password!' }] },\n ]"}],attrs:{type:"password",placeholder:"Password"}},[r("a-icon",{staticStyle:{color:"rgba(0,0,0,.25)"},attrs:{slot:"prefix",type:"lock"},slot:"prefix"})],1)],1),r("a-form-item",[r("a-button",{attrs:{type:"primary",block:!0,"html-type":"submit",loading:e.loading}},[e._v(" 安装 ")])],1)],1),r("footer",[e._v(" Copyright © 2020 - "+e._s(e.thisYear)+" 0xJacky ")])],1)},n=[function(){var e=this,t=e.$createElement,r=e._self._c||t;return r("div",{staticClass:"project-title"},[r("h1",[e._v("Nginx UI")])])}],i=r("1da1"),o=(r("96cf"),{name:"Login",data:function(){return{form:{},lock:!0,thisYear:(new Date).getFullYear(),loading:!1}},created:function(){this.form=this.$form.createForm(this)},mounted:function(){var e=this;this.$api.install.get_lock().then((function(t){t.lock&&e.$router.push("/login")}))},methods:{handleSubmit:function(){var e=Object(i["a"])(regeneratorRuntime.mark((function e(t){var r=this;return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:return t.preventDefault(),this.loading=!0,e.next=4,this.form.validateFields(function(){var e=Object(i["a"])(regeneratorRuntime.mark((function e(t,a){return regeneratorRuntime.wrap((function(e){while(1)switch(e.prev=e.next){case 0:t||r.$api.install.install_nginx_ui(a).then((function(){r.$router.push("/login")})),r.loading=!1;case 2:case"end":return e.stop()}}),e)})));return function(t,r){return e.apply(this,arguments)}}());case 4:case"end":return e.stop()}}),e,this)})));function t(t){return e.apply(this,arguments)}return t}()}}),s=o,l=(r("0f6e"),r("2877")),u=Object(l["a"])(s,a,n,!1,null,null,null);t["default"]=u.exports}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-6a4ca29d"],{"06de":function(e,t,a){"use strict";a.r(t);var n=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("a-card",{staticStyle:{"text-align":"center"}},[a("div",{staticClass:"logo"},[a("img",{attrs:{src:e.logo,alt:"logo"}})]),a("h2",[e._v("Nginx UI")]),a("p",[e._v("Yet another WebUI for Nginx")]),a("p",[e._v("Version: "+e._s(e.version)+" ("+e._s(e.build_id)+")")]),a("h3",[e._v("项目组")]),a("p",[e._v("Designer"),a("a",{attrs:{href:"https://jackyu.cn/"}},[e._v("@0xJacky")])]),a("h3",[e._v("技术栈")]),a("p",[e._v("Go")]),a("p",[e._v("Gin")]),a("p",[e._v("Vue")]),a("p",[e._v("Websocket")]),a("h3",[e._v("开源协议")]),a("p",[e._v("GNU General Public License v2.0")]),a("p",[e._v("Copyright © 2020 - "+e._s(e.this_year)+" 0xJacky ")])])},r=[],o=a("1da1"),s=(a("96cf"),{name:"About",data:function(){var e,t=new Date;return{logo:a("4ffd"),this_year:t.getFullYear(),version:"1.1.0",build_id:null!==(e="19")&&void 0!==e?e:"开发模式",api_root:"/api"}},methods:{changeUserPower:function(e){var t=this;return Object(o["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.next=2,t.$store.dispatch("update_mock_user",{power:e});case 2:return a.next=4,t.$api.user.info();case 4:return a.next=6,t.$message.success("修改成功");case 6:case"end":return a.stop()}}),a)})))()}}}),c=s,i=(a("a695"),a("2877")),u=Object(i["a"])(c,n,r,!1,null,"bb72cb78",null);t["default"]=u.exports},"0c2d":function(e,t,a){var n=a("24fb");t=n(!1),t.push([e.i,".logo img[data-v-bb72cb78]{max-width:120px}.egg[data-v-bb72cb78]{padding:10px 0}.ant-btn[data-v-bb72cb78]{margin:10px 10px 0 0}",""]),e.exports=t},"4ffd":function(e,t,a){e.exports=a.p+"img/logo.9e691c6b.png"},5683:function(e,t,a){var n=a("0c2d");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var r=a("499e").default;r("fc536a1c",n,!0,{sourceMap:!1,shadowMode:!1})},a695:function(e,t,a){"use strict";a("5683")}}]);

View file

@ -1 +0,0 @@
(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-6a4ca29d"],{"06de":function(e,t,a){"use strict";a.r(t);var n=function(){var e=this,t=e.$createElement,a=e._self._c||t;return a("a-card",{staticStyle:{"text-align":"center"}},[a("div",{staticClass:"logo"},[a("img",{attrs:{src:e.logo,alt:"logo"}})]),a("h2",[e._v("Nginx UI")]),a("p",[e._v("Yet another WebUI for Nginx")]),a("p",[e._v("Version: "+e._s(e.version)+" ("+e._s(e.build_id)+")")]),a("h3",[e._v("项目组")]),a("p",[e._v("Designer"),a("a",{attrs:{href:"https://jackyu.cn/"}},[e._v("@0xJacky")])]),a("h3",[e._v("技术栈")]),a("p",[e._v("Go")]),a("p",[e._v("Gin")]),a("p",[e._v("Vue")]),a("p",[e._v("Websocket")]),a("h3",[e._v("开源协议")]),a("p",[e._v("GNU General Public License v2.0")]),a("p",[e._v("Copyright © 2020 - "+e._s(e.this_year)+" 0xJacky ")])])},r=[],o=a("1da1"),s=(a("96cf"),{name:"About",data:function(){var e,t=new Date;return{logo:a("4ffd"),this_year:t.getFullYear(),version:"1.1.0",build_id:null!==(e="19")&&void 0!==e?e:"开发模式",api_root:"/api"}},methods:{changeUserPower:function(e){var t=this;return Object(o["a"])(regeneratorRuntime.mark((function a(){return regeneratorRuntime.wrap((function(a){while(1)switch(a.prev=a.next){case 0:return a.next=2,t.$store.dispatch("update_mock_user",{power:e});case 2:return a.next=4,t.$api.user.info();case 4:return a.next=6,t.$message.success("修改成功");case 6:case"end":return a.stop()}}),a)})))()}}}),c=s,i=(a("a695"),a("2877")),u=Object(i["a"])(c,n,r,!1,null,"bb72cb78",null);t["default"]=u.exports},"0c2d":function(e,t,a){var n=a("24fb");t=n(!1),t.push([e.i,".logo img[data-v-bb72cb78]{max-width:120px}.egg[data-v-bb72cb78]{padding:10px 0}.ant-btn[data-v-bb72cb78]{margin:10px 10px 0 0}",""]),e.exports=t},"4ffd":function(e,t,a){e.exports=a.p+"img/logo.9e691c6b.png"},5683:function(e,t,a){var n=a("0c2d");n.__esModule&&(n=n.default),"string"===typeof n&&(n=[[e.i,n,""]]),n.locals&&(e.exports=n.locals);var r=a("499e").default;r("fc536a1c",n,!0,{sourceMap:!1,shadowMode:!1})},a695:function(e,t,a){"use strict";a("5683")}}]);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
{"version":"1.1.0","build_id":2,"total_build":19}

6
frontend/frontend.go Normal file
View file

@ -0,0 +1,6 @@
package frontend
import "embed"
//go:embed dist
var DistFS embed.FS

View file

@ -1 +1 @@
{"version":"1.1.0","build_id":2,"total_build":19}
{"version":"1.1.0","build_id":3,"total_build":20}

View file

@ -1,13 +1,14 @@
module github.com/0xJacky/Nginx-UI/server
module github.com/0xJacky/Nginx-UI
go 1.16
require (
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/gin-contrib/static v0.0.1
github.com/gin-gonic/gin v1.7.4
github.com/go-acme/lego/v4 v4.4.0
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/locales v0.13.0
github.com/go-playground/universal-translator v0.17.0
github.com/go-playground/validator/v10 v10.4.1
github.com/google/uuid v1.1.1

View file

@ -107,6 +107,9 @@ github.com/getkin/kin-openapi v0.13.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-contrib/static v0.0.1 h1:JVxuvHPuUfkoul12N7dtQw7KRn/pSMq7Ue1Va9Swm1U=
github.com/gin-contrib/static v0.0.1/go.mod h1:CSxeF+wep05e0kCOsqWdAWbSszmc31zTIbD8TvWl7Hs=
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
github.com/gin-gonic/gin v1.7.4 h1:QmUZXrvJ9qZ3GfWvQ+2wnW/1ePrTEJqPKMYEU3lD/DM=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/go-acme/lego/v4 v4.4.0 h1:uHhU5LpOYQOdp3aDU+XY2bajseu8fuExphTL1Ss6/Fc=
@ -128,6 +131,7 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI=
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8=

32
main.go Normal file
View file

@ -0,0 +1,32 @@
package main
import (
"flag"
"github.com/0xJacky/Nginx-UI/model"
"github.com/0xJacky/Nginx-UI/router"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/0xJacky/Nginx-UI/tool"
"log"
)
func main() {
var dataDir string
flag.StringVar(&dataDir, "d", ".", "Specify the data dir")
flag.Parse()
settings.Init(dataDir)
model.Init()
r := router.InitRouter()
log.Printf("nginx config dir path: %s", tool.GetNginxConfPath(""))
go tool.AutoCert()
err := r.Run(":" + settings.ServerSettings.HttpPort)
if err != nil {
log.Fatal(err)
}
}

63
model/auth.go Normal file
View file

@ -0,0 +1,63 @@
package model
import (
"github.com/0xJacky/Nginx-UI/settings"
"github.com/dgrijalva/jwt-go"
"time"
)
type Auth struct {
Model
Name string `json:"name"`
Password string `json:"-"`
}
type AuthToken struct {
Token string `json:"token"`
}
type JWTClaims struct {
Name string `json:"name"`
jwt.StandardClaims
}
func GetUser(name string) (user Auth, err error) {
err = db.Where("name = ?", name).First(&user).Error
if err != nil {
return Auth{}, err
}
return user, err
}
func DeleteToken(token string) error {
return db.Where("token = ?", token).Delete(&AuthToken{}).Error
}
func CheckToken(token string) int64 {
return db.Where("token = ?", token).Find(&AuthToken{}).RowsAffected
}
func GenerateJWT(name string) (string, error) {
claims := JWTClaims{
Name: name,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
},
}
unsignedToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := unsignedToken.SignedString([]byte(settings.ServerSettings.JwtSecret))
if err != nil {
return "", err
}
err = db.Create(&AuthToken{
Token: signedToken,
}).Error
if err != nil {
return "", err
}
return signedToken, err
}

View file

@ -1,7 +1,7 @@
package model
import (
"github.com/0xJacky/Nginx-UI/server/settings"
"github.com/0xJacky/Nginx-UI/settings"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"

134
router/routers.go Normal file
View file

@ -0,0 +1,134 @@
package router
import (
"bufio"
"encoding/base64"
"github.com/0xJacky/Nginx-UI/api"
"github.com/0xJacky/Nginx-UI/frontend"
"github.com/0xJacky/Nginx-UI/model"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"io/fs"
"log"
"net/http"
"path/filepath"
"strings"
)
func authRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
tmp, _ := base64.StdEncoding.DecodeString(c.Query("token"))
token = string(tmp)
if token == "" {
c.JSON(http.StatusForbidden, gin.H{
"message": "auth fail",
})
c.Abort()
return
}
}
n := model.CheckToken(token)
if n < 1 {
c.JSON(http.StatusForbidden, gin.H{
"message": "auth fail",
})
c.Abort()
return
}
c.Next()
}
}
type serverFileSystemType struct {
http.FileSystem
}
func (f serverFileSystemType) Exists(prefix string, path string) bool {
_, err := f.Open(filepath.Join(prefix, path))
return err == nil
}
func mustFS(dir string) (serverFileSystem static.ServeFileSystem) {
sub, err := fs.Sub(frontend.DistFS, filepath.Join("public", dir))
if err != nil {
log.Println(err)
}
serverFileSystem = serverFileSystemType{
http.FS(sub),
}
return
}
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.Use(static.Serve("/", mustFS("")))
r.NoRoute(func(c *gin.Context) {
accept := c.Request.Header.Get("Accept")
if strings.Contains(accept, "text/html") {
file, _ := mustFS("").Open("index.html")
stat, _ := file.Stat()
c.DataFromReader(http.StatusOK, stat.Size(), "text/html",
bufio.NewReader(file), nil)
}
})
g := r.Group("/api")
{
g.GET("install", api.InstallLockCheck)
g.POST("install", api.InstallNginxUI)
g.POST("/login", api.Login)
g.DELETE("/logout", api.Logout)
g := g.Group("/", authRequired())
{
g.GET("/analytic", api.Analytic)
g.GET("/users", api.GetUsers)
g.GET("/user/:id", api.GetUser)
g.POST("/user", api.AddUser)
g.POST("/user/:id", api.EditUser)
g.DELETE("/user/:id", api.DeleteUser)
g.GET("domains", api.GetDomains)
g.GET("domain/:name", api.GetDomain)
g.POST("domain/:name", api.EditDomain)
g.POST("domain/:name/enable", api.EnableDomain)
g.POST("domain/:name/disable", api.DisableDomain)
g.DELETE("domain/:name", api.DeleteDomain)
g.GET("configs", api.GetConfigs)
g.GET("config/:name", api.GetConfig)
g.POST("config", api.AddConfig)
g.POST("config/:name", api.EditConfig)
g.GET("backups", api.GetFileBackupList)
g.GET("backup/:id", api.GetFileBackup)
g.GET("template/:name", api.GetTemplate)
g.GET("cert/issue/:domain", api.IssueCert)
g.GET("cert/:domain/info", api.CertInfo)
// 添加域名到自动续期列表
g.POST("cert/:domain", api.AddDomainToAutoCert)
// 从自动续期列表中删除域名
g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
}
}
return r
}

View file

@ -1,57 +0,0 @@
package api
import (
"github.com/0xJacky/Nginx-UI/server/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)
}

View file

@ -1,263 +0,0 @@
package api
import (
"github.com/0xJacky/Nginx-UI/server/model"
"github.com/0xJacky/Nginx-UI/server/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)
}

View file

@ -1,37 +0,0 @@
package api
import (
"github.com/0xJacky/Nginx-UI/server/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,
})
}

View file

@ -1,32 +0,0 @@
package main
import (
"flag"
"github.com/0xJacky/Nginx-UI/server/model"
"github.com/0xJacky/Nginx-UI/server/router"
"github.com/0xJacky/Nginx-UI/server/settings"
"github.com/0xJacky/Nginx-UI/server/tool"
"log"
)
func main() {
var dataDir string
flag.StringVar(&dataDir, "d", ".", "Specify the data dir")
flag.Parse()
settings.Init(dataDir)
model.Init()
r := router.InitRouter()
log.Printf("nginx config dir path: %s", tool.GetNginxConfPath(""))
go tool.AutoCert()
err := r.Run(":" + settings.ServerSettings.HttpPort)
if err != nil {
log.Fatal(err)
}
}

View file

@ -1,63 +0,0 @@
package model
import (
"github.com/0xJacky/Nginx-UI/server/settings"
"github.com/dgrijalva/jwt-go"
"time"
)
type Auth struct {
Model
Name string `json:"name"`
Password string `json:"-"`
}
type AuthToken struct {
Token string `json:"token"`
}
type JWTClaims struct {
Name string `json:"name"`
jwt.StandardClaims
}
func GetUser(name string) (user Auth, err error){
err = db.Where("name = ?", name).First(&user).Error
if err != nil {
return Auth{}, err
}
return user, err
}
func DeleteToken(token string) error {
return db.Where("token = ?", token).Delete(&AuthToken{}).Error
}
func CheckToken(token string) int64 {
return db.Where("token = ?", token).Find(&AuthToken{}).RowsAffected
}
func GenerateJWT(name string) (string, error) {
claims := JWTClaims{
Name: name,
StandardClaims: jwt.StandardClaims{
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
},
}
unsignedToken := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signedToken, err := unsignedToken.SignedString([]byte(settings.ServerSettings.JwtSecret))
if err != nil {
return "", err
}
err = db.Create(&AuthToken{
Token: signedToken,
}).Error
if err != nil {
return "", err
}
return signedToken, err
}

View file

@ -1,94 +0,0 @@
package router
import (
"encoding/base64"
"github.com/0xJacky/Nginx-UI/server/api"
"github.com/0xJacky/Nginx-UI/server/model"
"github.com/gin-gonic/gin"
"net/http"
)
func authRequired() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
tmp, _ := base64.StdEncoding.DecodeString(c.Query("token"))
token = string(tmp)
if token == "" {
c.JSON(http.StatusForbidden, gin.H{
"message": "auth fail",
})
c.Abort()
return
}
}
n := model.CheckToken(token)
if n < 1 {
c.JSON(http.StatusForbidden, gin.H{
"message": "auth fail",
})
c.Abort()
return
}
c.Next()
}
}
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.GET("/", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "Hello World",
})
})
r.GET("install", api.InstallLockCheck)
r.POST("install", api.InstallNginxUI)
r.POST("/login", api.Login)
r.DELETE("/logout", api.Logout)
g := r.Group("/", authRequired())
{
r.GET("/analytic", api.Analytic)
g.GET("/users", api.GetUsers)
g.GET("/user/:id", api.GetUser)
g.POST("/user", api.AddUser)
g.POST("/user/:id", api.EditUser)
g.DELETE("/user/:id", api.DeleteUser)
g.GET("domains", api.GetDomains)
g.GET("domain/:name", api.GetDomain)
g.POST("domain/:name", api.EditDomain)
g.POST("domain/:name/enable", api.EnableDomain)
g.POST("domain/:name/disable", api.DisableDomain)
g.DELETE("domain/:name", api.DeleteDomain)
g.GET("configs", api.GetConfigs)
g.GET("config/:name", api.GetConfig)
g.POST("config", api.AddConfig)
g.POST("config/:name", api.EditConfig)
g.GET("backups", api.GetFileBackupList)
g.GET("backup/:id", api.GetFileBackup)
g.GET("template/:name", api.GetTemplate)
g.GET("cert/issue/:domain", api.IssueCert)
g.GET("cert/:domain/info", api.CertInfo)
// 添加域名到自动续期列表
g.POST("cert/:domain", api.AddDomainToAutoCert)
// 从自动续期列表中删除域名
g.DELETE("cert/:domain", api.RemoveDomainFromAutoCert)
}
return r
}

View file

@ -1,59 +0,0 @@
package test
import (
"fmt"
"github.com/0xJacky/Nginx-UI/server/tool"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"testing"
)
func TestAcme(t *testing.T) {
const acmePath = "/usr/local/acme.sh"
_, err := os.Stat(acmePath)
log.Println("[found] acme.sh ", acmePath)
if err != nil {
log.Println(err)
if os.IsNotExist(err) {
log.Println("[not found] acme.sh, installing...")
if _, err := os.Stat("../tmp"); os.IsNotExist(err) {
_ = os.Mkdir("../tmp", 0644)
}
out, err := exec.Command("curl", "-o", "../tmp/acme.sh", "https://get.acme.sh").
CombinedOutput()
if err != nil {
log.Println(err)
return
}
fmt.Printf("%s\n", out)
log.Println("[acme.sh] downloaded")
file, _ := ioutil.ReadFile("../tmp/acme.sh")
fileString := string(file)
fileString = strings.Replace(fileString, "https://raw.githubusercontent.com",
"https://ghproxy.com/https://raw.githubusercontent.com", -1)
_ = ioutil.WriteFile("../tmp/acme.sh", []byte(fileString), 0644)
out, err = exec.Command("bash", "../tmp/acme.sh",
"install",
"--log",
"--home", "/usr/local/acme.sh",
"--cert-home", tool.GetNginxConfPath("ssl")).
CombinedOutput()
if err != nil {
log.Println(err)
return
}
fmt.Printf("%s\n", out)
}
}
}

View file

@ -1,40 +0,0 @@
package test
import (
"fmt"
"github.com/0xJacky/Nginx-UI/server/tool"
"log"
"os"
"os/exec"
"testing"
)
func TestCert(t *testing.T) {
out, err := exec.Command("bash", "/usr/local/acme.sh/acme.sh",
"--issue",
"-d", "test.ojbk.me",
"--nginx").CombinedOutput()
if err != nil {
log.Println(err)
return
}
fmt.Printf("%s\n", out)
_, err = os.Stat(tool.GetNginxConfPath("ssl/test.ojbk.me/fullchain.cer"))
if err != nil {
log.Println(err)
return
}
log.Println("[found]", "fullchain.cer")
_, err = os.Stat(tool.GetNginxConfPath("ssl/test.ojbk.me/test.ojbk.me.key"))
if err != nil {
log.Println(err)
return
}
log.Println("[found]", "cert key")
log.Println("申请成功")
}

View file

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

59
test/acme_test.go Normal file
View file

@ -0,0 +1,59 @@
package test
import (
"fmt"
"github.com/0xJacky/Nginx-UI/tool"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"testing"
)
func TestAcme(t *testing.T) {
const acmePath = "/usr/local/acme.sh"
_, err := os.Stat(acmePath)
log.Println("[found] acme.sh ", acmePath)
if err != nil {
log.Println(err)
if os.IsNotExist(err) {
log.Println("[not found] acme.sh, installing...")
if _, err := os.Stat("../tmp"); os.IsNotExist(err) {
_ = os.Mkdir("../tmp", 0644)
}
out, err := exec.Command("curl", "-o", "../tmp/acme.sh", "https://get.acme.sh").
CombinedOutput()
if err != nil {
log.Println(err)
return
}
fmt.Printf("%s\n", out)
log.Println("[acme.sh] downloaded")
file, _ := ioutil.ReadFile("../tmp/acme.sh")
fileString := string(file)
fileString = strings.Replace(fileString, "https://raw.githubusercontent.com",
"https://ghproxy.com/https://raw.githubusercontent.com", -1)
_ = ioutil.WriteFile("../tmp/acme.sh", []byte(fileString), 0644)
out, err = exec.Command("bash", "../tmp/acme.sh",
"install",
"--log",
"--home", "/usr/local/acme.sh",
"--cert-home", tool.GetNginxConfPath("ssl")).
CombinedOutput()
if err != nil {
log.Println(err)
return
}
fmt.Printf("%s\n", out)
}
}
}

40
test/cert_test.go Normal file
View file

@ -0,0 +1,40 @@
package test
import (
"fmt"
"github.com/0xJacky/Nginx-UI/tool"
"log"
"os"
"os/exec"
"testing"
)
func TestCert(t *testing.T) {
out, err := exec.Command("bash", "/usr/local/acme.sh/acme.sh",
"--issue",
"-d", "test.ojbk.me",
"--nginx").CombinedOutput()
if err != nil {
log.Println(err)
return
}
fmt.Printf("%s\n", out)
_, err = os.Stat(tool.GetNginxConfPath("ssl/test.ojbk.me/fullchain.cer"))
if err != nil {
log.Println(err)
return
}
log.Println("[found]", "fullchain.cer")
_, err = os.Stat(tool.GetNginxConfPath("ssl/test.ojbk.me/test.ojbk.me.key"))
if err != nil {
log.Println(err)
return
}
log.Println("[found]", "cert key")
log.Println("申请成功")
}

169
tool/cert.go Normal file
View file

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