refactor: remove pages, combine grpc http port
This commit is contained in:
		
							parent
							
								
									4fc0aad7a0
								
							
						
					
					
						commit
						606e10ca0a
					
				@ -7,7 +7,6 @@ import (
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/mygin"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -18,30 +17,30 @@ type apiV1 struct {
 | 
			
		||||
func (v *apiV1) serve() {
 | 
			
		||||
	r := v.r.Group("")
 | 
			
		||||
	// 强制认证的 API
 | 
			
		||||
	r.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		MemberOnly: true,
 | 
			
		||||
		AllowAPI:   true,
 | 
			
		||||
		IsPage:     false,
 | 
			
		||||
		Msg:        "访问此接口需要认证",
 | 
			
		||||
		Btn:        "点此登录",
 | 
			
		||||
		Redirect:   "/login",
 | 
			
		||||
	}))
 | 
			
		||||
	// r.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
	// 	MemberOnly: true,
 | 
			
		||||
	// 	AllowAPI:   true,
 | 
			
		||||
	// 	IsPage:     false,
 | 
			
		||||
	// 	Msg:        "访问此接口需要认证",
 | 
			
		||||
	// 	Btn:        "点此登录",
 | 
			
		||||
	// 	Redirect:   "/login",
 | 
			
		||||
	// }))
 | 
			
		||||
	r.GET("/server/list", v.serverList)
 | 
			
		||||
	r.GET("/server/details", v.serverDetails)
 | 
			
		||||
	// 不强制认证的 API
 | 
			
		||||
	mr := v.r.Group("monitor")
 | 
			
		||||
	mr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		MemberOnly: false,
 | 
			
		||||
		IsPage:     false,
 | 
			
		||||
		AllowAPI:   true,
 | 
			
		||||
		Msg:        "访问此接口需要认证",
 | 
			
		||||
		Btn:        "点此登录",
 | 
			
		||||
		Redirect:   "/login",
 | 
			
		||||
	}))
 | 
			
		||||
	mr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
 | 
			
		||||
		IsPage:        false,
 | 
			
		||||
		AbortWhenFail: true,
 | 
			
		||||
	}))
 | 
			
		||||
	// mr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
	// 	MemberOnly: false,
 | 
			
		||||
	// 	IsPage:     false,
 | 
			
		||||
	// 	AllowAPI:   true,
 | 
			
		||||
	// 	Msg:        "访问此接口需要认证",
 | 
			
		||||
	// 	Btn:        "点此登录",
 | 
			
		||||
	// 	Redirect:   "/login",
 | 
			
		||||
	// }))
 | 
			
		||||
	// mr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
 | 
			
		||||
	// 	IsPage:        false,
 | 
			
		||||
	// 	AbortWhenFail: true,
 | 
			
		||||
	// }))
 | 
			
		||||
	mr.GET("/:id", v.monitorHistoriesById)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -10,11 +10,9 @@ import (
 | 
			
		||||
	"github.com/gorilla/websocket"
 | 
			
		||||
	"github.com/hashicorp/go-uuid"
 | 
			
		||||
	"github.com/jinzhu/copier"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
	"golang.org/x/sync/singleflight"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/mygin"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/utils"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/websocketx"
 | 
			
		||||
	"github.com/naiba/nezha/proto"
 | 
			
		||||
@ -29,14 +27,11 @@ type commonPage struct {
 | 
			
		||||
 | 
			
		||||
func (cp *commonPage) serve() {
 | 
			
		||||
	cr := cp.r.Group("")
 | 
			
		||||
	cr.Use(mygin.Authorize(mygin.AuthorizeOption{}))
 | 
			
		||||
	cr.Use(mygin.PreferredTheme)
 | 
			
		||||
	cr.POST("/view-password", cp.issueViewPassword)
 | 
			
		||||
	cr.GET("/terminal/:id", cp.terminal)
 | 
			
		||||
	cr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
 | 
			
		||||
		IsPage:        true,
 | 
			
		||||
		AbortWhenFail: true,
 | 
			
		||||
	}))
 | 
			
		||||
	// cr.Use(mygin.ValidateViewPassword(mygin.ValidateViewPasswordOption{
 | 
			
		||||
	// 	IsPage:        true,
 | 
			
		||||
	// 	AbortWhenFail: true,
 | 
			
		||||
	// }))
 | 
			
		||||
	cr.GET("/", cp.home)
 | 
			
		||||
	cr.GET("/service", cp.service)
 | 
			
		||||
	// TODO: 界面直接跳转使用该接口
 | 
			
		||||
@ -48,44 +43,6 @@ func (cp *commonPage) serve() {
 | 
			
		||||
	cr.GET("/file/:id", cp.fm)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type viewPasswordForm struct {
 | 
			
		||||
	Password string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PingExample godoc
 | 
			
		||||
// @Summary ping example
 | 
			
		||||
// @Schemes
 | 
			
		||||
// @Description do ping
 | 
			
		||||
// @Tags example
 | 
			
		||||
// @Accept json
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {string} Helloworld
 | 
			
		||||
// @Router /example/helloworld [get]
 | 
			
		||||
func (p *commonPage) issueViewPassword(c *gin.Context) {
 | 
			
		||||
	var vpf viewPasswordForm
 | 
			
		||||
	err := c.ShouldBind(&vpf)
 | 
			
		||||
	var hash []byte
 | 
			
		||||
	if err == nil && vpf.Password != singleton.Conf.Site.ViewPassword {
 | 
			
		||||
		// err = errors.New(singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "WrongAccessPassword"}))
 | 
			
		||||
	}
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		hash, err = bcrypt.GenerateFromPassword([]byte(vpf.Password), bcrypt.DefaultCost)
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code: http.StatusOK,
 | 
			
		||||
			// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
			// 	MessageID: "AnErrorEccurred",
 | 
			
		||||
			// }),
 | 
			
		||||
			Msg: err.Error(),
 | 
			
		||||
		}, true)
 | 
			
		||||
		c.Abort()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	c.SetCookie(singleton.Conf.Site.CookieName+"-vp", string(hash), 60*60*24, "", "", false, false)
 | 
			
		||||
	c.Redirect(http.StatusFound, c.Request.Referer())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *commonPage) service(c *gin.Context) {
 | 
			
		||||
	res, _, _ := p.requestGroup.Do("servicePage", func() (interface{}, error) {
 | 
			
		||||
		singleton.AlertsLock.RLock()
 | 
			
		||||
@ -104,11 +61,11 @@ func (p *commonPage) service(c *gin.Context) {
 | 
			
		||||
			stats, statsStore,
 | 
			
		||||
		}, nil
 | 
			
		||||
	})
 | 
			
		||||
	c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/service"), mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "", gin.H{
 | 
			
		||||
		// "Title":              singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesStatus"}),
 | 
			
		||||
		"Services":           res.([]interface{})[0],
 | 
			
		||||
		"CycleTransferStats": res.([]interface{})[1],
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cp *commonPage) network(c *gin.Context) {
 | 
			
		||||
@ -124,13 +81,13 @@ func (cp *commonPage) network(c *gin.Context) {
 | 
			
		||||
	}
 | 
			
		||||
	if err := singleton.DB.Model(&model.MonitorHistory{}).Select("monitor_id, server_id").
 | 
			
		||||
		Where("monitor_id != 0 and server_id != 0").Limit(1).First(&monitorHistory).Error; err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "请求失败",
 | 
			
		||||
			Msg:   "请求参数有误:" + "server monitor history not found",
 | 
			
		||||
			Link:  "/",
 | 
			
		||||
			Btn:   "返回重试",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "请求失败",
 | 
			
		||||
		// 	Msg:   "请求参数有误:" + "server monitor history not found",
 | 
			
		||||
		// 	Link:  "/",
 | 
			
		||||
		// 	Btn:   "返回重试",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	} else {
 | 
			
		||||
		if monitorHistory == nil || monitorHistory.ServerID == 0 {
 | 
			
		||||
@ -147,24 +104,24 @@ func (cp *commonPage) network(c *gin.Context) {
 | 
			
		||||
		var err error
 | 
			
		||||
		id, err = strconv.ParseUint(idStr, 10, 64)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
				Code:  http.StatusForbidden,
 | 
			
		||||
				Title: "请求失败",
 | 
			
		||||
				Msg:   "请求参数有误:" + err.Error(),
 | 
			
		||||
				Link:  "/",
 | 
			
		||||
				Btn:   "返回重试",
 | 
			
		||||
			}, true)
 | 
			
		||||
			// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			// 	Code:  http.StatusForbidden,
 | 
			
		||||
			// 	Title: "请求失败",
 | 
			
		||||
			// 	Msg:   "请求参数有误:" + err.Error(),
 | 
			
		||||
			// 	Link:  "/",
 | 
			
		||||
			// 	Btn:   "返回重试",
 | 
			
		||||
			// }, true)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		_, ok := singleton.ServerList[id]
 | 
			
		||||
		if !ok {
 | 
			
		||||
			mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
				Code:  http.StatusForbidden,
 | 
			
		||||
				Title: "请求失败",
 | 
			
		||||
				Msg:   "请求参数有误:" + "server id not found",
 | 
			
		||||
				Link:  "/",
 | 
			
		||||
				Btn:   "返回重试",
 | 
			
		||||
			}, true)
 | 
			
		||||
			// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			// 	Code:  http.StatusForbidden,
 | 
			
		||||
			// 	Title: "请求失败",
 | 
			
		||||
			// 	Msg:   "请求参数有误:" + "server id not found",
 | 
			
		||||
			// 	Link:  "/",
 | 
			
		||||
			// 	Btn:   "返回重试",
 | 
			
		||||
			// }, true)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
@ -178,13 +135,13 @@ func (cp *commonPage) network(c *gin.Context) {
 | 
			
		||||
		Where("server_id != 0").
 | 
			
		||||
		Find(&serverIdsWithMonitor).
 | 
			
		||||
		Error; err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "请求失败",
 | 
			
		||||
			Msg:   "请求参数有误:" + "no server with monitor histories",
 | 
			
		||||
			Link:  "/",
 | 
			
		||||
			Btn:   "返回重试",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "请求失败",
 | 
			
		||||
		// 	Msg:   "请求参数有误:" + "no server with monitor histories",
 | 
			
		||||
		// 	Link:  "/",
 | 
			
		||||
		// 	Btn:   "返回重试",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	if isMember || isViewPasswordVerfied {
 | 
			
		||||
@ -209,11 +166,10 @@ func (cp *commonPage) network(c *gin.Context) {
 | 
			
		||||
		Servers: servers,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/network"), mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
		"Servers":         string(serversBytes),
 | 
			
		||||
		"MonitorInfos":    string(monitorInfos),
 | 
			
		||||
		"MaxTCPPingValue": singleton.Conf.MaxTCPPingValue,
 | 
			
		||||
	}))
 | 
			
		||||
	c.HTML(http.StatusOK, "", gin.H{
 | 
			
		||||
		"Servers":      string(serversBytes),
 | 
			
		||||
		"MonitorInfos": string(monitorInfos),
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cp *commonPage) getServerStat(c *gin.Context, withPublicNote bool) ([]byte, error) {
 | 
			
		||||
@ -251,20 +207,20 @@ func (cp *commonPage) getServerStat(c *gin.Context, withPublicNote bool) ([]byte
 | 
			
		||||
func (cp *commonPage) home(c *gin.Context) {
 | 
			
		||||
	stat, err := cp.getServerStat(c, true)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code: http.StatusInternalServerError,
 | 
			
		||||
			// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
			// 	MessageID: "SystemError",
 | 
			
		||||
			// }),
 | 
			
		||||
			Msg:  "服务器状态获取失败",
 | 
			
		||||
			Link: "/",
 | 
			
		||||
			Btn:  "返回首页",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code: http.StatusInternalServerError,
 | 
			
		||||
		// 	// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
		// 	// 	MessageID: "SystemError",
 | 
			
		||||
		// 	// }),
 | 
			
		||||
		// 	Msg:  "服务器状态获取失败",
 | 
			
		||||
		// 	Link: "/",
 | 
			
		||||
		// 	Btn:  "返回首页",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	c.HTML(http.StatusOK, mygin.GetPreferredTheme(c, "/home"), mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "", gin.H{
 | 
			
		||||
		"Servers": string(stat),
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var upgrader = websocket.Upgrader{
 | 
			
		||||
@ -280,15 +236,15 @@ type Data struct {
 | 
			
		||||
func (cp *commonPage) ws(c *gin.Context) {
 | 
			
		||||
	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code: http.StatusInternalServerError,
 | 
			
		||||
			// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
			// 	MessageID: "NetworkError",
 | 
			
		||||
			// }),
 | 
			
		||||
			Msg:  "Websocket协议切换失败",
 | 
			
		||||
			Link: "/",
 | 
			
		||||
			Btn:  "返回首页",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code: http.StatusInternalServerError,
 | 
			
		||||
		// 	// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
		// 	// 	MessageID: "NetworkError",
 | 
			
		||||
		// 	// }),
 | 
			
		||||
		// 	Msg:  "Websocket协议切换失败",
 | 
			
		||||
		// 	Link: "/",
 | 
			
		||||
		// 	Btn:  "返回首页",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer conn.Close()
 | 
			
		||||
@ -315,28 +271,28 @@ func (cp *commonPage) ws(c *gin.Context) {
 | 
			
		||||
func (cp *commonPage) terminal(c *gin.Context) {
 | 
			
		||||
	streamId := c.Param("id")
 | 
			
		||||
	if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "无权访问",
 | 
			
		||||
			Msg:   "终端会话不存在",
 | 
			
		||||
			Link:  "/",
 | 
			
		||||
			Btn:   "返回首页",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "无权访问",
 | 
			
		||||
		// 	Msg:   "终端会话不存在",
 | 
			
		||||
		// 	Link:  "/",
 | 
			
		||||
		// 	Btn:   "返回首页",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
 | 
			
		||||
 | 
			
		||||
	wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code: http.StatusInternalServerError,
 | 
			
		||||
			// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
			// 	MessageID: "NetworkError",
 | 
			
		||||
			// }),
 | 
			
		||||
			Msg:  "Websocket协议切换失败",
 | 
			
		||||
			Link: "/",
 | 
			
		||||
			Btn:  "返回首页",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code: http.StatusInternalServerError,
 | 
			
		||||
		// 	// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
		// 	// 	MessageID: "NetworkError",
 | 
			
		||||
		// 	// }),
 | 
			
		||||
		// 	Msg:  "Websocket协议切换失败",
 | 
			
		||||
		// 	Link: "/",
 | 
			
		||||
		// 	Btn:  "返回首页",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer wsConn.Close()
 | 
			
		||||
@ -367,38 +323,38 @@ type createTerminalRequest struct {
 | 
			
		||||
 | 
			
		||||
func (cp *commonPage) createTerminal(c *gin.Context) {
 | 
			
		||||
	if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "无权访问",
 | 
			
		||||
			Msg:   "用户未登录",
 | 
			
		||||
			Link:  "/login",
 | 
			
		||||
			Btn:   "去登录",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "无权访问",
 | 
			
		||||
		// 	Msg:   "用户未登录",
 | 
			
		||||
		// 	Link:  "/login",
 | 
			
		||||
		// 	Btn:   "去登录",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	var createTerminalReq createTerminalRequest
 | 
			
		||||
	if err := c.ShouldBind(&createTerminalReq); err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "请求失败",
 | 
			
		||||
			Msg:   "请求参数有误:" + err.Error(),
 | 
			
		||||
			Link:  "/server",
 | 
			
		||||
			Btn:   "返回重试",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "请求失败",
 | 
			
		||||
		// 	Msg:   "请求参数有误:" + err.Error(),
 | 
			
		||||
		// 	Link:  "/server",
 | 
			
		||||
		// 	Btn:   "返回重试",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	streamId, err := uuid.GenerateUUID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code: http.StatusInternalServerError,
 | 
			
		||||
			// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
			// 	MessageID: "SystemError",
 | 
			
		||||
			// }),
 | 
			
		||||
			Msg:  "生成会话ID失败",
 | 
			
		||||
			Link: "/server",
 | 
			
		||||
			Btn:  "返回重试",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code: http.StatusInternalServerError,
 | 
			
		||||
		// 	// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
		// 	// 	MessageID: "SystemError",
 | 
			
		||||
		// 	// }),
 | 
			
		||||
		// 	Msg:  "生成会话ID失败",
 | 
			
		||||
		// 	Link: "/server",
 | 
			
		||||
		// 	Btn:  "返回重试",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -408,13 +364,13 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
 | 
			
		||||
	server := singleton.ServerList[createTerminalReq.ID]
 | 
			
		||||
	singleton.ServerLock.RUnlock()
 | 
			
		||||
	if server == nil || server.TaskStream == nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "请求失败",
 | 
			
		||||
			Msg:   "服务器不存在或处于离线状态",
 | 
			
		||||
			Link:  "/server",
 | 
			
		||||
			Btn:   "返回重试",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "请求失败",
 | 
			
		||||
		// 	Msg:   "服务器不存在或处于离线状态",
 | 
			
		||||
		// 	Link:  "/server",
 | 
			
		||||
		// 	Btn:   "返回重试",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -425,48 +381,48 @@ func (cp *commonPage) createTerminal(c *gin.Context) {
 | 
			
		||||
		Type: model.TaskTypeTerminalGRPC,
 | 
			
		||||
		Data: string(terminalData),
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "请求失败",
 | 
			
		||||
			Msg:   "Agent信令下发失败",
 | 
			
		||||
			Link:  "/server",
 | 
			
		||||
			Btn:   "返回重试",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "请求失败",
 | 
			
		||||
		// 	Msg:   "Agent信令下发失败",
 | 
			
		||||
		// 	Link:  "/server",
 | 
			
		||||
		// 	Btn:   "返回重试",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/terminal", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "", gin.H{
 | 
			
		||||
		"SessionID":  streamId,
 | 
			
		||||
		"ServerName": server.Name,
 | 
			
		||||
		"ServerID":   server.ID,
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (cp *commonPage) fm(c *gin.Context) {
 | 
			
		||||
	streamId := c.Param("id")
 | 
			
		||||
	if _, err := rpc.NezhaHandlerSingleton.GetStream(streamId); err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "无权访问",
 | 
			
		||||
			Msg:   "FM会话不存在",
 | 
			
		||||
			Link:  "/",
 | 
			
		||||
			Btn:   "返回首页",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "无权访问",
 | 
			
		||||
		// 	Msg:   "FM会话不存在",
 | 
			
		||||
		// 	Link:  "/",
 | 
			
		||||
		// 	Btn:   "返回首页",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer rpc.NezhaHandlerSingleton.CloseStream(streamId)
 | 
			
		||||
 | 
			
		||||
	wsConn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code: http.StatusInternalServerError,
 | 
			
		||||
			// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
			// 	MessageID: "NetworkError",
 | 
			
		||||
			// }),
 | 
			
		||||
			Msg:  "Websocket协议切换失败",
 | 
			
		||||
			Link: "/",
 | 
			
		||||
			Btn:  "返回首页",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code: http.StatusInternalServerError,
 | 
			
		||||
		// 	// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
		// 	// 	MessageID: "NetworkError",
 | 
			
		||||
		// 	// }),
 | 
			
		||||
		// 	Msg:  "Websocket协议切换失败",
 | 
			
		||||
		// 	Link: "/",
 | 
			
		||||
		// 	Btn:  "返回首页",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	defer wsConn.Close()
 | 
			
		||||
@ -492,27 +448,27 @@ func (cp *commonPage) fm(c *gin.Context) {
 | 
			
		||||
func (cp *commonPage) createFM(c *gin.Context) {
 | 
			
		||||
	IdString := c.Query("id")
 | 
			
		||||
	if _, authorized := c.Get(model.CtxKeyAuthorizedUser); !authorized {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "无权访问",
 | 
			
		||||
			Msg:   "用户未登录",
 | 
			
		||||
			Link:  "/login",
 | 
			
		||||
			Btn:   "去登录",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "无权访问",
 | 
			
		||||
		// 	Msg:   "用户未登录",
 | 
			
		||||
		// 	Link:  "/login",
 | 
			
		||||
		// 	Btn:   "去登录",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	streamId, err := uuid.GenerateUUID()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code: http.StatusInternalServerError,
 | 
			
		||||
			// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
			// 	MessageID: "SystemError",
 | 
			
		||||
			// }),
 | 
			
		||||
			Msg:  "生成会话ID失败",
 | 
			
		||||
			Link: "/server",
 | 
			
		||||
			Btn:  "返回重试",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code: http.StatusInternalServerError,
 | 
			
		||||
		// 	// Title: singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
		// 	// 	MessageID: "SystemError",
 | 
			
		||||
		// 	// }),
 | 
			
		||||
		// 	Msg:  "生成会话ID失败",
 | 
			
		||||
		// 	Link: "/server",
 | 
			
		||||
		// 	Btn:  "返回重试",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -520,13 +476,13 @@ func (cp *commonPage) createFM(c *gin.Context) {
 | 
			
		||||
 | 
			
		||||
	serverId, err := strconv.Atoi(IdString)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "请求失败",
 | 
			
		||||
			Msg:   "请求参数有误:" + err.Error(),
 | 
			
		||||
			Link:  "/server",
 | 
			
		||||
			Btn:   "返回重试",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "请求失败",
 | 
			
		||||
		// 	Msg:   "请求参数有误:" + err.Error(),
 | 
			
		||||
		// 	Link:  "/server",
 | 
			
		||||
		// 	Btn:   "返回重试",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -534,13 +490,13 @@ func (cp *commonPage) createFM(c *gin.Context) {
 | 
			
		||||
	server := singleton.ServerList[uint64(serverId)]
 | 
			
		||||
	singleton.ServerLock.RUnlock()
 | 
			
		||||
	if server == nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "请求失败",
 | 
			
		||||
			Msg:   "服务器不存在或处于离线状态",
 | 
			
		||||
			Link:  "/server",
 | 
			
		||||
			Btn:   "返回重试",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "请求失败",
 | 
			
		||||
		// 	Msg:   "服务器不存在或处于离线状态",
 | 
			
		||||
		// 	Link:  "/server",
 | 
			
		||||
		// 	Btn:   "返回重试",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@ -551,17 +507,17 @@ func (cp *commonPage) createFM(c *gin.Context) {
 | 
			
		||||
		Type: model.TaskTypeFM,
 | 
			
		||||
		Data: string(fmData),
 | 
			
		||||
	}); err != nil {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusForbidden,
 | 
			
		||||
			Title: "请求失败",
 | 
			
		||||
			Msg:   "Agent信令下发失败",
 | 
			
		||||
			Link:  "/server",
 | 
			
		||||
			Btn:   "返回重试",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusForbidden,
 | 
			
		||||
		// 	Title: "请求失败",
 | 
			
		||||
		// 	Msg:   "Agent信令下发失败",
 | 
			
		||||
		// 	Link:  "/server",
 | 
			
		||||
		// 	Btn:   "返回重试",
 | 
			
		||||
		// }, true)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/file", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-", gin.H{
 | 
			
		||||
		"SessionID": streamId,
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,6 +4,7 @@ import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	jwt "github.com/appleboy/gin-jwt/v2"
 | 
			
		||||
@ -15,7 +16,6 @@ import (
 | 
			
		||||
 | 
			
		||||
	docs "github.com/naiba/nezha/cmd/dashboard/docs"
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/mygin"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/utils"
 | 
			
		||||
	"github.com/naiba/nezha/proto"
 | 
			
		||||
	"github.com/naiba/nezha/service/rpc"
 | 
			
		||||
@ -41,7 +41,7 @@ import (
 | 
			
		||||
 | 
			
		||||
// @externalDocs.description  OpenAPI
 | 
			
		||||
// @externalDocs.url          https://swagger.io/resources/open-api/
 | 
			
		||||
func ServeWeb(port uint) *http.Server {
 | 
			
		||||
func ServeWeb() *http.Server {
 | 
			
		||||
	gin.SetMode(gin.ReleaseMode)
 | 
			
		||||
	r := gin.Default()
 | 
			
		||||
	docs.SwaggerInfo.BasePath = "/api/v1"
 | 
			
		||||
@ -50,23 +50,24 @@ func ServeWeb(port uint) *http.Server {
 | 
			
		||||
		pprof.Register(r)
 | 
			
		||||
	}
 | 
			
		||||
	r.Use(natGateway)
 | 
			
		||||
	r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
 | 
			
		||||
	r.Use(mygin.RecordPath)
 | 
			
		||||
	if singleton.Conf.Debug {
 | 
			
		||||
		r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerfiles.Handler))
 | 
			
		||||
	}
 | 
			
		||||
	r.Use(recordPath)
 | 
			
		||||
	routers(r)
 | 
			
		||||
	page404 := func(c *gin.Context) {
 | 
			
		||||
		mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
			Code:  http.StatusNotFound,
 | 
			
		||||
			Title: "该页面不存在",
 | 
			
		||||
			Msg:   "该页面内容可能已着陆火星",
 | 
			
		||||
			Link:  "/",
 | 
			
		||||
			Btn:   "返回首页",
 | 
			
		||||
		}, true)
 | 
			
		||||
		// mygin.ShowErrorPage(c, mygin.ErrInfo{
 | 
			
		||||
		// 	Code:  http.StatusNotFound,
 | 
			
		||||
		// 	Title: "该页面不存在",
 | 
			
		||||
		// 	Msg:   "该页面内容可能已着陆火星",
 | 
			
		||||
		// 	Link:  "/",
 | 
			
		||||
		// 	Btn:   "返回首页",
 | 
			
		||||
		// }, true)
 | 
			
		||||
	}
 | 
			
		||||
	r.NoRoute(page404)
 | 
			
		||||
	r.NoMethod(page404)
 | 
			
		||||
 | 
			
		||||
	srv := &http.Server{
 | 
			
		||||
		Addr:              fmt.Sprintf(":%d", port),
 | 
			
		||||
		ReadHeaderTimeout: time.Second * 5,
 | 
			
		||||
		Handler:           r,
 | 
			
		||||
	}
 | 
			
		||||
@ -90,9 +91,6 @@ func routers(r *gin.Engine) {
 | 
			
		||||
	// 通用页面
 | 
			
		||||
	cp := commonPage{r: r}
 | 
			
		||||
	cp.serve()
 | 
			
		||||
	// 游客页面
 | 
			
		||||
	gp := guestPage{r}
 | 
			
		||||
	gp.serve()
 | 
			
		||||
	// 会员页面
 | 
			
		||||
	mp := &memberPage{r}
 | 
			
		||||
	mp.serve()
 | 
			
		||||
@ -164,3 +162,11 @@ func natGateway(c *gin.Context) {
 | 
			
		||||
	rpc.NezhaHandlerSingleton.StartStream(streamId, time.Second*10)
 | 
			
		||||
	c.Abort()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func recordPath(c *gin.Context) {
 | 
			
		||||
	url := c.Request.URL.String()
 | 
			
		||||
	for _, p := range c.Params {
 | 
			
		||||
		url = strings.Replace(url, p.Value, ":"+p.Key, 1)
 | 
			
		||||
	}
 | 
			
		||||
	c.Set("MatchedPath", url)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,62 +0,0 @@
 | 
			
		||||
package controller
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/mygin"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type guestPage struct {
 | 
			
		||||
	r *gin.Engine
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (gp *guestPage) serve() {
 | 
			
		||||
	gr := gp.r.Group("")
 | 
			
		||||
	gr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		GuestOnly: true,
 | 
			
		||||
		IsPage:    true,
 | 
			
		||||
		Msg:       "您已登录",
 | 
			
		||||
		Btn:       "返回首页",
 | 
			
		||||
		Redirect:  "/",
 | 
			
		||||
	}))
 | 
			
		||||
 | 
			
		||||
	gr.GET("/login", gp.login)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (gp *guestPage) login(c *gin.Context) {
 | 
			
		||||
	if singleton.Conf.Oauth2.OidcAutoLogin {
 | 
			
		||||
		c.Redirect(http.StatusFound, "/oauth2/login")
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	LoginType := "GitHub"
 | 
			
		||||
	RegistrationLink := "https://github.com/join"
 | 
			
		||||
	if singleton.Conf.Oauth2.Type == model.ConfigTypeGitee {
 | 
			
		||||
		LoginType = "Gitee"
 | 
			
		||||
		RegistrationLink = "https://gitee.com/signup"
 | 
			
		||||
	} else if singleton.Conf.Oauth2.Type == model.ConfigTypeGitlab {
 | 
			
		||||
		LoginType = "Gitlab"
 | 
			
		||||
		RegistrationLink = "https://gitlab.com/users/sign_up"
 | 
			
		||||
	} else if singleton.Conf.Oauth2.Type == model.ConfigTypeJihulab {
 | 
			
		||||
		LoginType = "Jihulab"
 | 
			
		||||
		RegistrationLink = "https://jihulab.com/users/sign_up"
 | 
			
		||||
	} else if singleton.Conf.Oauth2.Type == model.ConfigTypeGitea {
 | 
			
		||||
		LoginType = "Gitea"
 | 
			
		||||
		RegistrationLink = fmt.Sprintf("%s/user/sign_up", singleton.Conf.Oauth2.Endpoint)
 | 
			
		||||
	} else if singleton.Conf.Oauth2.Type == model.ConfigTypeCloudflare {
 | 
			
		||||
		LoginType = "Cloudflare"
 | 
			
		||||
		RegistrationLink = "https://dash.cloudflare.com/sign-up/teams"
 | 
			
		||||
	} else if singleton.Conf.Oauth2.Type == model.ConfigTypeOidc {
 | 
			
		||||
		LoginType = singleton.Conf.Oauth2.OidcDisplayName
 | 
			
		||||
		RegistrationLink = singleton.Conf.Oauth2.OidcRegisterURL
 | 
			
		||||
	}
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/login", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
		// "Title":            singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
 | 
			
		||||
		"LoginType":        LoginType,
 | 
			
		||||
		"RegistrationLink": RegistrationLink,
 | 
			
		||||
	}))
 | 
			
		||||
}
 | 
			
		||||
@ -7,12 +7,14 @@ import (
 | 
			
		||||
	jwt "github.com/appleboy/gin-jwt/v2"
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func initParams() *jwt.GinJWTMiddleware {
 | 
			
		||||
	return &jwt.GinJWTMiddleware{
 | 
			
		||||
		Realm:       "test zone",
 | 
			
		||||
		Key:         []byte("secret key"),
 | 
			
		||||
		Realm:       singleton.Conf.SiteName,
 | 
			
		||||
		Key:         []byte(singleton.Conf.SecretKey),
 | 
			
		||||
		Timeout:     time.Hour,
 | 
			
		||||
		MaxRefresh:  time.Hour,
 | 
			
		||||
		IdentityKey: model.CtxKeyAuthorizedUser,
 | 
			
		||||
@ -41,7 +43,7 @@ func payloadFunc() func(data interface{}) jwt.MapClaims {
 | 
			
		||||
	return func(data interface{}) jwt.MapClaims {
 | 
			
		||||
		if v, ok := data.(*model.User); ok {
 | 
			
		||||
			return jwt.MapClaims{
 | 
			
		||||
				model.CtxKeyAuthorizedUser: v.Username,
 | 
			
		||||
				model.CtxKeyAuthorizedUser: v.ID,
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return jwt.MapClaims{}
 | 
			
		||||
@ -51,36 +53,48 @@ func payloadFunc() func(data interface{}) jwt.MapClaims {
 | 
			
		||||
func identityHandler() func(c *gin.Context) interface{} {
 | 
			
		||||
	return func(c *gin.Context) interface{} {
 | 
			
		||||
		claims := jwt.ExtractClaims(c)
 | 
			
		||||
		return &model.User{
 | 
			
		||||
			Username: claims[model.CtxKeyAuthorizedUser].(string),
 | 
			
		||||
		userId := claims[model.CtxKeyAuthorizedUser].(uint64)
 | 
			
		||||
		var user model.User
 | 
			
		||||
		if err := singleton.DB.First(&user, userId).Error; err != nil {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
		return &user
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// login test godoc
 | 
			
		||||
// @Summary ping example
 | 
			
		||||
// @Schemes
 | 
			
		||||
// @Description do ping
 | 
			
		||||
// @Tags example
 | 
			
		||||
// @Accept json
 | 
			
		||||
// @Produce json
 | 
			
		||||
// @Success 200 {string} Helloworld
 | 
			
		||||
// @Router /example/login [get]
 | 
			
		||||
func authenticator() func(c *gin.Context) (interface{}, error) {
 | 
			
		||||
	return func(c *gin.Context) (interface{}, error) {
 | 
			
		||||
		var loginVals model.LoginRequest
 | 
			
		||||
		if err := c.ShouldBind(&loginVals); err != nil {
 | 
			
		||||
			return "", jwt.ErrMissingLoginValues
 | 
			
		||||
		}
 | 
			
		||||
		userID := loginVals.Username
 | 
			
		||||
		password := loginVals.Password
 | 
			
		||||
 | 
			
		||||
		if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
 | 
			
		||||
			return &model.User{
 | 
			
		||||
				Username: userID,
 | 
			
		||||
			}, nil
 | 
			
		||||
		var user model.User
 | 
			
		||||
		if err := singleton.DB.Select("id").Where("username = ?", loginVals.Username).First(&user).Error; err != nil {
 | 
			
		||||
			return nil, jwt.ErrFailedAuthentication
 | 
			
		||||
		}
 | 
			
		||||
		return nil, jwt.ErrFailedAuthentication
 | 
			
		||||
 | 
			
		||||
		if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(loginVals.Password)); err != nil {
 | 
			
		||||
			return nil, jwt.ErrFailedAuthentication
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return &user, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func authorizator() func(data interface{}, c *gin.Context) bool {
 | 
			
		||||
	return func(data interface{}, c *gin.Context) bool {
 | 
			
		||||
		if v, ok := data.(*model.User); ok && v.Username == "admin" {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
		return false
 | 
			
		||||
		_, ok := data.(*model.User)
 | 
			
		||||
		return ok
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -16,7 +16,6 @@ import (
 | 
			
		||||
	"gorm.io/gorm"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/mygin"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/utils"
 | 
			
		||||
	"github.com/naiba/nezha/proto"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
@ -28,13 +27,13 @@ type memberAPI struct {
 | 
			
		||||
 | 
			
		||||
func (ma *memberAPI) serve() {
 | 
			
		||||
	mr := ma.r.Group("")
 | 
			
		||||
	mr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		MemberOnly: true,
 | 
			
		||||
		IsPage:     false,
 | 
			
		||||
		Msg:        "访问此接口需要登录",
 | 
			
		||||
		Btn:        "点此登录",
 | 
			
		||||
		Redirect:   "/login",
 | 
			
		||||
	}))
 | 
			
		||||
	// mr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
	// 	MemberOnly: true,
 | 
			
		||||
	// 	IsPage:     false,
 | 
			
		||||
	// 	Msg:        "访问此接口需要登录",
 | 
			
		||||
	// 	Btn:        "点此登录",
 | 
			
		||||
	// 	Redirect:   "/login",
 | 
			
		||||
	// }))
 | 
			
		||||
 | 
			
		||||
	mr.GET("/search-server", ma.searchServer)
 | 
			
		||||
	mr.GET("/search-tasks", ma.searchTask)
 | 
			
		||||
@ -997,30 +996,23 @@ func (ma *memberAPI) logout(c *gin.Context) {
 | 
			
		||||
		Code: http.StatusOK,
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	if oidcLogoutUrl := singleton.Conf.Oauth2.OidcLogoutURL; oidcLogoutUrl != "" {
 | 
			
		||||
		// 重定向到 OIDC 退出登录地址。不知道为什么,这里的重定向不生效
 | 
			
		||||
		c.Redirect(http.StatusOK, oidcLogoutUrl)
 | 
			
		||||
	}
 | 
			
		||||
	// if oidcLogoutUrl := singleton.Conf.Oauth2.OidcLogoutURL; oidcLogoutUrl != "" {
 | 
			
		||||
	// 	// 重定向到 OIDC 退出登录地址。不知道为什么,这里的重定向不生效
 | 
			
		||||
	// 	c.Redirect(http.StatusOK, oidcLogoutUrl)
 | 
			
		||||
	// }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type settingForm struct {
 | 
			
		||||
	Title                   string
 | 
			
		||||
	Admin                   string
 | 
			
		||||
	SiteName                string
 | 
			
		||||
	Language                string
 | 
			
		||||
	Theme                   string
 | 
			
		||||
	DashboardTheme          string
 | 
			
		||||
	CustomCode              string
 | 
			
		||||
	CustomCodeDashboard     string
 | 
			
		||||
	CustomNameservers       string
 | 
			
		||||
	ViewPassword            string
 | 
			
		||||
	IgnoredIPNotification   string
 | 
			
		||||
	IPChangeNotificationTag string // IP变更提醒的通知组
 | 
			
		||||
	GRPCHost                string
 | 
			
		||||
	InstallHost             string
 | 
			
		||||
	Cover                   uint8
 | 
			
		||||
 | 
			
		||||
	EnableIPChangeNotification      string
 | 
			
		||||
	EnablePlainIPInNotification     string
 | 
			
		||||
	DisableSwitchTemplateInFrontend string
 | 
			
		||||
	EnableIPChangeNotification  string
 | 
			
		||||
	EnablePlainIPInNotification string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ma *memberAPI) updateSetting(c *gin.Context) {
 | 
			
		||||
@ -1033,38 +1025,31 @@ func (ma *memberAPI) updateSetting(c *gin.Context) {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, yes := model.Themes[sf.Theme]; !yes {
 | 
			
		||||
		c.JSON(http.StatusOK, model.Response{
 | 
			
		||||
			Code:    http.StatusBadRequest,
 | 
			
		||||
			Message: fmt.Sprintf("前台主题不存在:%s", sf.Theme),
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// if _, yes := model.Themes[sf.Theme]; !yes {
 | 
			
		||||
	// 	c.JSON(http.StatusOK, model.Response{
 | 
			
		||||
	// 		Code:    http.StatusBadRequest,
 | 
			
		||||
	// 		Message: fmt.Sprintf("前台主题不存在:%s", sf.Theme),
 | 
			
		||||
	// 	})
 | 
			
		||||
	// 	return
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	if _, yes := model.DashboardThemes[sf.DashboardTheme]; !yes {
 | 
			
		||||
		c.JSON(http.StatusOK, model.Response{
 | 
			
		||||
			Code:    http.StatusBadRequest,
 | 
			
		||||
			Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme),
 | 
			
		||||
		})
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	// if _, yes := model.DashboardThemes[sf.DashboardTheme]; !yes {
 | 
			
		||||
	// 	c.JSON(http.StatusOK, model.Response{
 | 
			
		||||
	// 		Code:    http.StatusBadRequest,
 | 
			
		||||
	// 		Message: fmt.Sprintf("后台主题不存在:%s", sf.DashboardTheme),
 | 
			
		||||
	// 	})
 | 
			
		||||
	// 	return
 | 
			
		||||
	// }
 | 
			
		||||
 | 
			
		||||
	singleton.Conf.Language = sf.Language
 | 
			
		||||
	singleton.Conf.EnableIPChangeNotification = sf.EnableIPChangeNotification == "on"
 | 
			
		||||
	singleton.Conf.EnablePlainIPInNotification = sf.EnablePlainIPInNotification == "on"
 | 
			
		||||
	singleton.Conf.DisableSwitchTemplateInFrontend = sf.DisableSwitchTemplateInFrontend == "on"
 | 
			
		||||
	singleton.Conf.Cover = sf.Cover
 | 
			
		||||
	// singleton.Conf.GRPCHost = sf.GRPCHost
 | 
			
		||||
	singleton.Conf.InstallHost = sf.InstallHost
 | 
			
		||||
	singleton.Conf.IgnoredIPNotification = sf.IgnoredIPNotification
 | 
			
		||||
	singleton.Conf.IPChangeNotificationTag = sf.IPChangeNotificationTag
 | 
			
		||||
	singleton.Conf.Site.Brand = sf.Title
 | 
			
		||||
	singleton.Conf.Site.Theme = sf.Theme
 | 
			
		||||
	singleton.Conf.Site.DashboardTheme = sf.DashboardTheme
 | 
			
		||||
	singleton.Conf.Site.CustomCode = sf.CustomCode
 | 
			
		||||
	singleton.Conf.Site.CustomCodeDashboard = sf.CustomCodeDashboard
 | 
			
		||||
	singleton.Conf.SiteName = sf.SiteName
 | 
			
		||||
	singleton.Conf.DNSServers = sf.CustomNameservers
 | 
			
		||||
	singleton.Conf.Site.ViewPassword = sf.ViewPassword
 | 
			
		||||
	singleton.Conf.Oauth2.Admin = sf.Admin
 | 
			
		||||
	// 保证NotificationTag不为空
 | 
			
		||||
	if singleton.Conf.IPChangeNotificationTag == "" {
 | 
			
		||||
		singleton.Conf.IPChangeNotificationTag = "default"
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import (
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/mygin"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@ -15,13 +14,13 @@ type memberPage struct {
 | 
			
		||||
 | 
			
		||||
func (mp *memberPage) serve() {
 | 
			
		||||
	mr := mp.r.Group("")
 | 
			
		||||
	mr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
		MemberOnly: true,
 | 
			
		||||
		IsPage:     true,
 | 
			
		||||
		// Msg:        singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
 | 
			
		||||
		// Btn:        singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
 | 
			
		||||
		Redirect: "/login",
 | 
			
		||||
	}))
 | 
			
		||||
	// mr.Use(mygin.Authorize(mygin.AuthorizeOption{
 | 
			
		||||
	// 	MemberOnly: true,
 | 
			
		||||
	// 	IsPage:     true,
 | 
			
		||||
	// 	// Msg:        singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "YouAreNotAuthorized"}),
 | 
			
		||||
	// 	// Btn:        singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Login"}),
 | 
			
		||||
	// 	Redirect: "/login",
 | 
			
		||||
	// }))
 | 
			
		||||
	mr.GET("/server", mp.server)
 | 
			
		||||
	mr.GET("/monitor", mp.monitor)
 | 
			
		||||
	mr.GET("/cron", mp.cron)
 | 
			
		||||
@ -35,35 +34,35 @@ func (mp *memberPage) serve() {
 | 
			
		||||
func (mp *memberPage) api(c *gin.Context) {
 | 
			
		||||
	singleton.ApiLock.RLock()
 | 
			
		||||
	defer singleton.ApiLock.RUnlock()
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/api", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-", gin.H{
 | 
			
		||||
		// "title":  singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ApiManagement"}),
 | 
			
		||||
		"Tokens": singleton.ApiTokenList,
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mp *memberPage) server(c *gin.Context) {
 | 
			
		||||
	singleton.SortedServerLock.RLock()
 | 
			
		||||
	defer singleton.SortedServerLock.RUnlock()
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/server", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-", gin.H{
 | 
			
		||||
		// "Title":   singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServersManagement"}),
 | 
			
		||||
		"Servers": singleton.SortedServerList,
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mp *memberPage) monitor(c *gin.Context) {
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/monitor", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-", gin.H{
 | 
			
		||||
		// "Title":    singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ServicesManagement"}),
 | 
			
		||||
		"Monitors": singleton.ServiceSentinelShared.Monitors(),
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mp *memberPage) cron(c *gin.Context) {
 | 
			
		||||
	var crons []model.Cron
 | 
			
		||||
	singleton.DB.Find(&crons)
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/cron", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-", gin.H{
 | 
			
		||||
		// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "ScheduledTasks"}),
 | 
			
		||||
		"Crons": crons,
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mp *memberPage) notification(c *gin.Context) {
 | 
			
		||||
@ -71,37 +70,37 @@ func (mp *memberPage) notification(c *gin.Context) {
 | 
			
		||||
	singleton.DB.Find(&nf)
 | 
			
		||||
	var ar []model.AlertRule
 | 
			
		||||
	singleton.DB.Find(&ar)
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/notification", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-", gin.H{
 | 
			
		||||
		// "Title":         singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Notification"}),
 | 
			
		||||
		"Notifications": nf,
 | 
			
		||||
		"AlertRules":    ar,
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mp *memberPage) ddns(c *gin.Context) {
 | 
			
		||||
	var data []model.DDNSProfile
 | 
			
		||||
	singleton.DB.Find(&data)
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/ddns", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-", gin.H{
 | 
			
		||||
		// "Title":        singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "DDNS"}),
 | 
			
		||||
		"DDNS":         data,
 | 
			
		||||
		"ProviderMap":  model.ProviderMap,
 | 
			
		||||
		"ProviderList": model.ProviderList,
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mp *memberPage) nat(c *gin.Context) {
 | 
			
		||||
	var data []model.NAT
 | 
			
		||||
	singleton.DB.Find(&data)
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/nat", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-", gin.H{
 | 
			
		||||
		// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "NAT"}),
 | 
			
		||||
		"NAT": data,
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (mp *memberPage) setting(c *gin.Context) {
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/setting", mygin.CommonEnvironment(c, gin.H{
 | 
			
		||||
	c.HTML(http.StatusOK, "dashboard-", gin.H{
 | 
			
		||||
		// "Title":           singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "Settings"}),
 | 
			
		||||
		"Languages":       model.Languages,
 | 
			
		||||
		"DashboardThemes": model.DashboardThemes,
 | 
			
		||||
	}))
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,18 +4,20 @@ import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net"
 | 
			
		||||
	"time"
 | 
			
		||||
	_ "time/tzdata"
 | 
			
		||||
 | 
			
		||||
	"github.com/ory/graceful"
 | 
			
		||||
	"github.com/soheilhy/cmux"
 | 
			
		||||
	flag "github.com/spf13/pflag"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/cmd/dashboard/controller"
 | 
			
		||||
	"github.com/naiba/nezha/cmd/dashboard/rpc"
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/proto"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
	"github.com/ory/graceful"
 | 
			
		||||
	flag "github.com/spf13/pflag"
 | 
			
		||||
	// gin-swagger middleware
 | 
			
		||||
	// swagger embed files
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type DashboardCliParam struct {
 | 
			
		||||
@ -43,6 +45,25 @@ func init() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initSystem() {
 | 
			
		||||
	// 初始化管理员账户
 | 
			
		||||
	var usersCount int64
 | 
			
		||||
	if err := singleton.DB.Model(&model.User{}).Count(&usersCount).Error; err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	if usersCount == 0 {
 | 
			
		||||
		hash, err := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			panic(err)
 | 
			
		||||
		}
 | 
			
		||||
		admin := model.User{
 | 
			
		||||
			Username: "admin",
 | 
			
		||||
			Password: string(hash),
 | 
			
		||||
		}
 | 
			
		||||
		if err := singleton.DB.Create(&admin).Error; err != nil {
 | 
			
		||||
			panic(err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 启动 singleton 包下的所有服务
 | 
			
		||||
	singleton.LoadSingleton()
 | 
			
		||||
 | 
			
		||||
@ -63,19 +84,28 @@ func main() {
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	l, err := net.Listen("tcp", fmt.Sprintf(":%d", singleton.Conf.ListenPort))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal(err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m := cmux.New(l)
 | 
			
		||||
	grpcL := m.Match(cmux.HTTP2HeaderField("content-type", "application/grpc"))
 | 
			
		||||
	httpL := m.Match(cmux.HTTP1Fast())
 | 
			
		||||
 | 
			
		||||
	// TODO 使用 cmux 在同一端口服务 HTTP 和 gRPC
 | 
			
		||||
	singleton.CleanMonitorHistory()
 | 
			
		||||
	go rpc.ServeRPC(singleton.Conf.ListenPort)
 | 
			
		||||
	go rpc.ServeRPC(grpcL)
 | 
			
		||||
	serviceSentinelDispatchBus := make(chan model.Monitor) // 用于传递服务监控任务信息的channel
 | 
			
		||||
	go rpc.DispatchTask(serviceSentinelDispatchBus)
 | 
			
		||||
	go rpc.DispatchKeepalive()
 | 
			
		||||
	go singleton.AlertSentinelStart()
 | 
			
		||||
	singleton.NewServiceSentinel(serviceSentinelDispatchBus)
 | 
			
		||||
	srv := controller.ServeWeb(singleton.Conf.ListenPort)
 | 
			
		||||
	srv := controller.ServeWeb()
 | 
			
		||||
 | 
			
		||||
	go dispatchReportInfoTask()
 | 
			
		||||
	if err := graceful.Graceful(func() error {
 | 
			
		||||
		return srv.ListenAndServe()
 | 
			
		||||
		return srv.Serve(httpL)
 | 
			
		||||
	}, func(c context.Context) error {
 | 
			
		||||
		log.Println("NEZHA>> Graceful::START")
 | 
			
		||||
		singleton.RecordTransferHourlyUsage()
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
package rpc
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net"
 | 
			
		||||
 | 
			
		||||
	"google.golang.org/grpc"
 | 
			
		||||
@ -12,15 +11,11 @@ import (
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func ServeRPC(port uint) {
 | 
			
		||||
func ServeRPC(l net.Listener) {
 | 
			
		||||
	server := grpc.NewServer()
 | 
			
		||||
	rpcService.NezhaHandlerSingleton = rpcService.NewNezhaHandler()
 | 
			
		||||
	pb.RegisterNezhaServiceServer(server, rpcService.NezhaHandlerSingleton)
 | 
			
		||||
	listen, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	server.Serve(listen)
 | 
			
		||||
	server.Serve(l)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func DispatchTask(serviceSentinelDispatchBus <-chan model.Monitor) {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							@ -20,6 +20,7 @@ require (
 | 
			
		||||
	github.com/oschwald/maxminddb-golang v1.13.1
 | 
			
		||||
	github.com/patrickmn/go-cache v2.1.0+incompatible
 | 
			
		||||
	github.com/robfig/cron/v3 v3.0.1
 | 
			
		||||
	github.com/soheilhy/cmux v0.1.5
 | 
			
		||||
	github.com/spf13/pflag v1.0.5
 | 
			
		||||
	github.com/spf13/viper v1.18.2
 | 
			
		||||
	github.com/swaggo/files v1.0.1
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										7
									
								
								go.sum
									
									
									
									
									
								
							@ -154,6 +154,8 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke
 | 
			
		||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
 | 
			
		||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
 | 
			
		||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
 | 
			
		||||
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
 | 
			
		||||
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
 | 
			
		||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
 | 
			
		||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
 | 
			
		||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
 | 
			
		||||
@ -206,6 +208,7 @@ go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTV
 | 
			
		||||
golang.org/x/arch v0.9.0 h1:ub9TgUInamJ8mrZIGlBG6/4TqWeMszd4N8lNorbrr6k=
 | 
			
		||||
golang.org/x/arch v0.9.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
			
		||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 | 
			
		||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
 | 
			
		||||
@ -215,7 +218,9 @@ golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqR
 | 
			
		||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
 | 
			
		||||
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
 | 
			
		||||
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 | 
			
		||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 | 
			
		||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 | 
			
		||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 | 
			
		||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
 | 
			
		||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
 | 
			
		||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 | 
			
		||||
@ -227,6 +232,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ
 | 
			
		||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
 | 
			
		||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 | 
			
		||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 | 
			
		||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ import (
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/pkg/utils"
 | 
			
		||||
	"github.com/spf13/viper"
 | 
			
		||||
	"sigs.k8s.io/yaml"
 | 
			
		||||
)
 | 
			
		||||
@ -77,40 +78,17 @@ func (c *AgentConfig) Save() error {
 | 
			
		||||
 | 
			
		||||
// Config 站点配置
 | 
			
		||||
type Config struct {
 | 
			
		||||
	Debug    bool   // debug模式开关
 | 
			
		||||
	Language string // 系统语言,默认 zh-CN
 | 
			
		||||
	Site     struct {
 | 
			
		||||
		Brand               string // 站点名称
 | 
			
		||||
		CookieName          string // 浏览器 Cookie 名称
 | 
			
		||||
		Theme               string
 | 
			
		||||
		DashboardTheme      string
 | 
			
		||||
		CustomCode          string
 | 
			
		||||
		CustomCodeDashboard string
 | 
			
		||||
		ViewPassword        string // 前台查看密码
 | 
			
		||||
	}
 | 
			
		||||
	Oauth2 struct {
 | 
			
		||||
		Type            string
 | 
			
		||||
		Admin           string // 管理员用户名列表
 | 
			
		||||
		AdminGroups     string // 管理员用户组列表
 | 
			
		||||
		ClientID        string
 | 
			
		||||
		ClientSecret    string
 | 
			
		||||
		Endpoint        string
 | 
			
		||||
		OidcDisplayName string // for OIDC Display Name
 | 
			
		||||
		OidcIssuer      string // for OIDC Issuer
 | 
			
		||||
		OidcLogoutURL   string // for OIDC Logout URL
 | 
			
		||||
		OidcRegisterURL string // for OIDC Register URL
 | 
			
		||||
		OidcLoginClaim  string // for OIDC Claim
 | 
			
		||||
		OidcGroupClaim  string // for OIDC Group Claim
 | 
			
		||||
		OidcScopes      string // for OIDC Scopes
 | 
			
		||||
		OidcAutoCreate  bool   // for OIDC Auto Create
 | 
			
		||||
		OidcAutoLogin   bool   // for OIDC Auto Login
 | 
			
		||||
	}
 | 
			
		||||
	Debug bool // debug模式开关
 | 
			
		||||
 | 
			
		||||
	Language    string // 系统语言,默认 zh-CN
 | 
			
		||||
	SiteName    string
 | 
			
		||||
	SecretKey   string
 | 
			
		||||
	ListenPort  uint
 | 
			
		||||
	InstallHost string
 | 
			
		||||
	TLS         bool
 | 
			
		||||
	Location    string // 时区,默认为 Asia/Shanghai
 | 
			
		||||
 | 
			
		||||
	EnablePlainIPInNotification     bool // 通知信息IP不打码
 | 
			
		||||
	DisableSwitchTemplateInFrontend bool // 前台禁用切换模板功能
 | 
			
		||||
	EnablePlainIPInNotification bool // 通知信息IP不打码
 | 
			
		||||
 | 
			
		||||
	// IP变更提醒
 | 
			
		||||
	EnableIPChangeNotification bool
 | 
			
		||||
@ -118,14 +96,11 @@ type Config struct {
 | 
			
		||||
	Cover                      uint8  // 覆盖范围(0:提醒未被 IgnoredIPNotification 包含的所有服务器; 1:仅提醒被 IgnoredIPNotification 包含的服务器;)
 | 
			
		||||
	IgnoredIPNotification      string // 特定服务器IP(多个服务器用逗号分隔)
 | 
			
		||||
 | 
			
		||||
	Location string // 时区,默认为 Asia/Shanghai
 | 
			
		||||
 | 
			
		||||
	v                              *viper.Viper
 | 
			
		||||
	IgnoredIPNotificationServerIDs map[uint64]bool // [ServerID] -> bool(值为true代表当前ServerID在特定服务器列表内)
 | 
			
		||||
	MaxTCPPingValue                int32
 | 
			
		||||
	AvgPingCount                   int
 | 
			
		||||
	DNSServers                     string
 | 
			
		||||
 | 
			
		||||
	DNSServers string
 | 
			
		||||
	v *viper.Viper
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Read 读取配置文件并应用
 | 
			
		||||
@ -142,12 +117,6 @@ func (c *Config) Read(path string) error {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if c.Site.Theme == "" {
 | 
			
		||||
		c.Site.Theme = "default"
 | 
			
		||||
	}
 | 
			
		||||
	if c.Site.DashboardTheme == "" {
 | 
			
		||||
		c.Site.DashboardTheme = "default"
 | 
			
		||||
	}
 | 
			
		||||
	if c.Language == "" {
 | 
			
		||||
		c.Language = "zh-CN"
 | 
			
		||||
	}
 | 
			
		||||
@ -157,23 +126,17 @@ func (c *Config) Read(path string) error {
 | 
			
		||||
	if c.Location == "" {
 | 
			
		||||
		c.Location = "Asia/Shanghai"
 | 
			
		||||
	}
 | 
			
		||||
	if c.MaxTCPPingValue == 0 {
 | 
			
		||||
		c.MaxTCPPingValue = 1000
 | 
			
		||||
	}
 | 
			
		||||
	if c.AvgPingCount == 0 {
 | 
			
		||||
		c.AvgPingCount = 2
 | 
			
		||||
	}
 | 
			
		||||
	if c.Oauth2.OidcScopes == "" {
 | 
			
		||||
		c.Oauth2.OidcScopes = "openid,profile,email"
 | 
			
		||||
	}
 | 
			
		||||
	if c.Oauth2.OidcLoginClaim == "" {
 | 
			
		||||
		c.Oauth2.OidcLoginClaim = "sub"
 | 
			
		||||
	}
 | 
			
		||||
	if c.Oauth2.OidcDisplayName == "" {
 | 
			
		||||
		c.Oauth2.OidcDisplayName = "OIDC"
 | 
			
		||||
	}
 | 
			
		||||
	if c.Oauth2.OidcGroupClaim == "" {
 | 
			
		||||
		c.Oauth2.OidcGroupClaim = "groups"
 | 
			
		||||
	if c.SecretKey == "" {
 | 
			
		||||
		c.SecretKey, err = utils.GenerateRandomString(1024)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		if err = c.Save(); err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	c.updateIgnoredIPNotificationID()
 | 
			
		||||
 | 
			
		||||
@ -1,82 +0,0 @@
 | 
			
		||||
package mygin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type AuthorizeOption struct {
 | 
			
		||||
	GuestOnly  bool
 | 
			
		||||
	MemberOnly bool
 | 
			
		||||
	IsPage     bool
 | 
			
		||||
	AllowAPI   bool
 | 
			
		||||
	Msg        string
 | 
			
		||||
	Redirect   string
 | 
			
		||||
	Btn        string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Authorize(opt AuthorizeOption) func(*gin.Context) {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		var code = http.StatusForbidden
 | 
			
		||||
		if opt.GuestOnly {
 | 
			
		||||
			code = http.StatusBadRequest
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		commonErr := ErrInfo{
 | 
			
		||||
			Title: "访问受限",
 | 
			
		||||
			Code:  code,
 | 
			
		||||
			Msg:   opt.Msg,
 | 
			
		||||
			Link:  opt.Redirect,
 | 
			
		||||
			Btn:   opt.Btn,
 | 
			
		||||
		}
 | 
			
		||||
		var isLogin bool
 | 
			
		||||
 | 
			
		||||
		// 用户鉴权
 | 
			
		||||
		token, _ := c.Cookie(singleton.Conf.Site.CookieName)
 | 
			
		||||
		token = strings.TrimSpace(token)
 | 
			
		||||
		if token != "" {
 | 
			
		||||
			var u model.User
 | 
			
		||||
			if err := singleton.DB.Where("token = ?", token).First(&u).Error; err == nil {
 | 
			
		||||
				isLogin = true // u.TokenExpired.After(time.Now())
 | 
			
		||||
			}
 | 
			
		||||
			if isLogin {
 | 
			
		||||
				c.Set(model.CtxKeyAuthorizedUser, &u)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// API鉴权
 | 
			
		||||
		if opt.AllowAPI {
 | 
			
		||||
			apiToken := c.GetHeader("Authorization")
 | 
			
		||||
			if apiToken != "" {
 | 
			
		||||
				var u model.User
 | 
			
		||||
				singleton.ApiLock.RLock()
 | 
			
		||||
				if _, ok := singleton.ApiTokenList[apiToken]; ok {
 | 
			
		||||
					err := singleton.DB.First(&u).Where("id = ?", singleton.ApiTokenList[apiToken].UserID).Error
 | 
			
		||||
					isLogin = err == nil
 | 
			
		||||
				}
 | 
			
		||||
				singleton.ApiLock.RUnlock()
 | 
			
		||||
				if isLogin {
 | 
			
		||||
					c.Set(model.CtxKeyAuthorizedUser, &u)
 | 
			
		||||
					c.Set("isAPI", true)
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 已登录且只能游客访问
 | 
			
		||||
		if isLogin && opt.GuestOnly {
 | 
			
		||||
			ShowErrorPage(c, commonErr, opt.IsPage)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// 未登录且需要登录
 | 
			
		||||
		if !isLogin && opt.MemberOnly {
 | 
			
		||||
			ShowErrorPage(c, commonErr, opt.IsPage)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,36 +0,0 @@
 | 
			
		||||
package mygin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ErrInfo struct {
 | 
			
		||||
	Code  int
 | 
			
		||||
	Title string
 | 
			
		||||
	Msg   string
 | 
			
		||||
	Link  string
 | 
			
		||||
	Btn   string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ShowErrorPage(c *gin.Context, i ErrInfo, isPage bool) {
 | 
			
		||||
	if isPage {
 | 
			
		||||
		c.HTML(i.Code, "dashboard-"+singleton.Conf.Site.DashboardTheme+"/error", CommonEnvironment(c, gin.H{
 | 
			
		||||
			"Code":  i.Code,
 | 
			
		||||
			"Title": i.Title,
 | 
			
		||||
			"Msg":   i.Msg,
 | 
			
		||||
			"Link":  i.Link,
 | 
			
		||||
			"Btn":   i.Btn,
 | 
			
		||||
		}))
 | 
			
		||||
	} else {
 | 
			
		||||
		c.JSON(http.StatusOK, model.Response{
 | 
			
		||||
			Code:    i.Code,
 | 
			
		||||
			Message: i.Msg,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	c.Abort()
 | 
			
		||||
}
 | 
			
		||||
@ -1,52 +0,0 @@
 | 
			
		||||
package mygin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var adminPage = map[string]bool{
 | 
			
		||||
	"/server":       true,
 | 
			
		||||
	"/monitor":      true,
 | 
			
		||||
	"/setting":      true,
 | 
			
		||||
	"/notification": true,
 | 
			
		||||
	"/ddns":         true,
 | 
			
		||||
	"/nat":          true,
 | 
			
		||||
	"/cron":         true,
 | 
			
		||||
	"/api":          true,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func CommonEnvironment(c *gin.Context, data map[string]interface{}) gin.H {
 | 
			
		||||
	data["MatchedPath"] = c.MustGet("MatchedPath")
 | 
			
		||||
	data["Version"] = singleton.Version
 | 
			
		||||
	data["Conf"] = singleton.Conf
 | 
			
		||||
	data["Themes"] = model.Themes
 | 
			
		||||
	data["CustomCode"] = singleton.Conf.Site.CustomCode
 | 
			
		||||
	data["CustomCodeDashboard"] = singleton.Conf.Site.CustomCodeDashboard
 | 
			
		||||
	// 是否是管理页面
 | 
			
		||||
	data["IsAdminPage"] = adminPage[data["MatchedPath"].(string)]
 | 
			
		||||
	// 站点标题
 | 
			
		||||
	if t, has := data["Title"]; !has {
 | 
			
		||||
		data["Title"] = singleton.Conf.Site.Brand
 | 
			
		||||
	} else {
 | 
			
		||||
		data["Title"] = fmt.Sprintf("%s - %s", t, singleton.Conf.Site.Brand)
 | 
			
		||||
	}
 | 
			
		||||
	u, ok := c.Get(model.CtxKeyAuthorizedUser)
 | 
			
		||||
	if ok {
 | 
			
		||||
		data["Admin"] = u
 | 
			
		||||
	}
 | 
			
		||||
	return data
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RecordPath(c *gin.Context) {
 | 
			
		||||
	url := c.Request.URL.String()
 | 
			
		||||
	for _, p := range c.Params {
 | 
			
		||||
		url = strings.Replace(url, p.Value, ":"+p.Key, 1)
 | 
			
		||||
	}
 | 
			
		||||
	c.Set("MatchedPath", url)
 | 
			
		||||
}
 | 
			
		||||
@ -1,30 +0,0 @@
 | 
			
		||||
package mygin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/pkg/utils"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func PreferredTheme(c *gin.Context) {
 | 
			
		||||
	// 采用前端传入的主题
 | 
			
		||||
	if theme, err := c.Cookie("preferred_theme"); err == nil {
 | 
			
		||||
		if _, has := model.Themes[theme]; has {
 | 
			
		||||
			// 检验自定义主题
 | 
			
		||||
			if theme == "custom" && singleton.Conf.Site.Theme != "custom" && !utils.IsFileExists("resource/template/theme-custom/home.html") {
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			c.Set(model.CtxKeyPreferredTheme, theme)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetPreferredTheme(c *gin.Context, path string) string {
 | 
			
		||||
	if theme, has := c.Get(model.CtxKeyPreferredTheme); has {
 | 
			
		||||
		return fmt.Sprintf("theme-%s%s", theme, path)
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("theme-%s%s", singleton.Conf.Site.Theme, path)
 | 
			
		||||
}
 | 
			
		||||
@ -1,50 +0,0 @@
 | 
			
		||||
package mygin
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"github.com/gin-gonic/gin"
 | 
			
		||||
	"github.com/naiba/nezha/model"
 | 
			
		||||
	"github.com/naiba/nezha/service/singleton"
 | 
			
		||||
	"golang.org/x/crypto/bcrypt"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type ValidateViewPasswordOption struct {
 | 
			
		||||
	IsPage        bool
 | 
			
		||||
	AbortWhenFail bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ValidateViewPassword(opt ValidateViewPasswordOption) gin.HandlerFunc {
 | 
			
		||||
	return func(c *gin.Context) {
 | 
			
		||||
		if singleton.Conf.Site.ViewPassword == "" {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		_, authorized := c.Get(model.CtxKeyAuthorizedUser)
 | 
			
		||||
		if authorized {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		viewPassword, err := c.Cookie(singleton.Conf.Site.CookieName + "-vp")
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			err = bcrypt.CompareHashAndPassword([]byte(viewPassword), []byte(singleton.Conf.Site.ViewPassword))
 | 
			
		||||
		}
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			c.Set(model.CtxKeyViewPasswordVerified, true)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if !opt.AbortWhenFail {
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		if opt.IsPage {
 | 
			
		||||
			c.HTML(http.StatusOK, GetPreferredTheme(c, "/viewpassword"), CommonEnvironment(c, gin.H{
 | 
			
		||||
				// "Title": singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{MessageID: "VerifyPassword"}),
 | 
			
		||||
			}))
 | 
			
		||||
 | 
			
		||||
		} else {
 | 
			
		||||
			c.JSON(http.StatusOK, model.Response{
 | 
			
		||||
				Code:    http.StatusForbidden,
 | 
			
		||||
				Message: "访问受限",
 | 
			
		||||
			})
 | 
			
		||||
		}
 | 
			
		||||
		c.Abort()
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
@ -1,33 +0,0 @@
 | 
			
		||||
package utils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"embed"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// HybridFS combines embed.FS and os.DirFS.
 | 
			
		||||
type HybridFS struct {
 | 
			
		||||
	embedFS, dir fs.FS
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewHybridFS(embed embed.FS, subDir string, localDir string) (*HybridFS, error) {
 | 
			
		||||
	subFS, err := fs.Sub(embed, subDir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &HybridFS{
 | 
			
		||||
		embedFS: subFS,
 | 
			
		||||
		dir:     os.DirFS(localDir),
 | 
			
		||||
	}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (hfs *HybridFS) Open(name string) (fs.File, error) {
 | 
			
		||||
	// Ensure embed files are not replaced
 | 
			
		||||
	if file, err := hfs.embedFS.Open(name); err == nil {
 | 
			
		||||
		return file, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return hfs.dir.Open(name)
 | 
			
		||||
}
 | 
			
		||||
@ -156,16 +156,16 @@ func (s *NezhaHandler) ReportSystemInfo(c context.Context, r *pb.Host) (*pb.Rece
 | 
			
		||||
		host.IP != "" &&
 | 
			
		||||
		singleton.ServerList[clientID].Host.IP != host.IP {
 | 
			
		||||
 | 
			
		||||
		singleton.SendNotification(singleton.Conf.IPChangeNotificationTag,
 | 
			
		||||
			fmt.Sprintf(
 | 
			
		||||
				"[%s] %s, %s => %s",
 | 
			
		||||
				// singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
				// 	MessageID: "IPChanged",
 | 
			
		||||
				// }),
 | 
			
		||||
				singleton.ServerList[clientID].Name, singleton.IPDesensitize(singleton.ServerList[clientID].Host.IP),
 | 
			
		||||
				singleton.IPDesensitize(host.IP),
 | 
			
		||||
			),
 | 
			
		||||
			nil)
 | 
			
		||||
		// singleton.SendNotification(singleton.Conf.IPChangeNotificationTag,
 | 
			
		||||
		// 	fmt.Sprintf(
 | 
			
		||||
		// 		"[%s] %s, %s => %s",
 | 
			
		||||
		// 		singleton.Localizer.MustLocalize(&i18n.LocalizeConfig{
 | 
			
		||||
		// 			MessageID: "IPChanged",
 | 
			
		||||
		// 		}),
 | 
			
		||||
		// 		singleton.ServerList[clientID].Name, singleton.IPDesensitize(singleton.ServerList[clientID].Host.IP),
 | 
			
		||||
		// 		singleton.IPDesensitize(host.IP),
 | 
			
		||||
		// 	),
 | 
			
		||||
		// 	nil)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/**
 | 
			
		||||
 | 
			
		||||
@ -350,9 +350,6 @@ func (ss *ServiceSentinel) worker() {
 | 
			
		||||
			ts.count++
 | 
			
		||||
			ts.ping = (ts.ping*float32(ts.count-1) + mh.Delay) / float32(ts.count)
 | 
			
		||||
			if ts.count == Conf.AvgPingCount {
 | 
			
		||||
				if ts.ping > float32(Conf.MaxTCPPingValue) {
 | 
			
		||||
					ts.ping = float32(Conf.MaxTCPPingValue)
 | 
			
		||||
				}
 | 
			
		||||
				ts.count = 0
 | 
			
		||||
				if err := DB.Create(&model.MonitorHistory{
 | 
			
		||||
					MonitorID: mh.GetId(),
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user