diff --git a/cmd/dashboard/controller/api_v1.go b/cmd/dashboard/controller/api_v1.go index c6c632d..112e4cd 100644 --- a/cmd/dashboard/controller/api_v1.go +++ b/cmd/dashboard/controller/api_v1.go @@ -99,7 +99,7 @@ func (v *apiV1) monitorHistoriesById(c *gin.Context) { } _, isMember := c.Get(model.CtxKeyAuthorizedUser) - _, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified) + var isViewPasswordVerfied bool authorized := isMember || isViewPasswordVerfied if server.HideForGuest && !authorized { diff --git a/cmd/dashboard/controller/common_page.go b/cmd/dashboard/controller/common_page.go index f357ff3..1fabf36 100644 --- a/cmd/dashboard/controller/common_page.go +++ b/cmd/dashboard/controller/common_page.go @@ -128,7 +128,7 @@ func (cp *commonPage) network(c *gin.Context) { monitorHistories := singleton.MonitorAPI.GetMonitorHistories(map[string]any{"server_id": id}) monitorInfos, _ = utils.Json.Marshal(monitorHistories) _, isMember := c.Get(model.CtxKeyAuthorizedUser) - _, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified) + var isViewPasswordVerfied bool if err := singleton.DB.Model(&model.MonitorHistory{}). Select("distinct(server_id)"). @@ -174,7 +174,7 @@ func (cp *commonPage) network(c *gin.Context) { func (cp *commonPage) getServerStat(c *gin.Context, withPublicNote bool) ([]byte, error) { _, isMember := c.Get(model.CtxKeyAuthorizedUser) - _, isViewPasswordVerfied := c.Get(model.CtxKeyViewPasswordVerified) + var isViewPasswordVerfied bool authorized := isMember || isViewPasswordVerfied v, err, _ := cp.requestGroup.Do(fmt.Sprintf("serverStats::%t", authorized), func() (interface{}, error) { singleton.SortedServerLock.RLock() diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index 3ab17d7..f2adeeb 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -22,25 +22,6 @@ import ( "github.com/naiba/nezha/service/singleton" ) -// @title Swagger Example API -// @version 1.0 -// @description This is a sample server celler server. -// @termsOfService http://swagger.io/terms/ - -// @contact.name API Support -// @contact.url http://www.swagger.io/support -// @contact.email support@swagger.io - -// @license.name Apache 2.0 -// @license.url http://www.apache.org/licenses/LICENSE-2.0.html - -// @host localhost:8080 -// @BasePath /api/v1 - -// @securityDefinitions.basic BasicAuth - -// @externalDocs.description OpenAPI -// @externalDocs.url https://swagger.io/resources/open-api/ func ServeWeb() *http.Server { gin.SetMode(gin.ReleaseMode) r := gin.Default() @@ -53,25 +34,14 @@ func ServeWeb() *http.Server { 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) - } - r.NoRoute(page404) - r.NoMethod(page404) - srv := &http.Server{ + return &http.Server{ ReadHeaderTimeout: time.Second * 5, Handler: r, } - return srv } func routers(r *gin.Engine) { @@ -79,27 +49,26 @@ func routers(r *gin.Engine) { if err != nil { log.Fatal("JWT Error:" + err.Error()) } + api := r.Group("api/v1") + api.Use(handlerMiddleWare(authMiddleware)) - // register middleware - r.Use(handlerMiddleWare(authMiddleware)) + api.POST("/login", authMiddleware.LoginHandler) - r.POST("/login", authMiddleware.LoginHandler) - - auth := r.Group("/auth", authMiddleware.MiddlewareFunc()) + auth := api.Group("", authMiddleware.MiddlewareFunc()) auth.GET("/refresh_token", authMiddleware.RefreshHandler) // 通用页面 - cp := commonPage{r: r} - cp.serve() - // 会员页面 - mp := &memberPage{r} - mp.serve() - // API - api := r.Group("api") - { - ma := &memberAPI{api} - ma.serve() - } + // cp := commonPage{r: r} + // cp.serve() + // // 会员页面 + // mp := &memberPage{r} + // mp.serve() + // // API + // external := api.Group("api") + // { + // ma := &memberAPI{external} + // ma.serve() + // } } func natGateway(c *gin.Context) { diff --git a/cmd/dashboard/controller/jwt.go b/cmd/dashboard/controller/jwt.go index 4eaf9a6..1ae5733 100644 --- a/cmd/dashboard/controller/jwt.go +++ b/cmd/dashboard/controller/jwt.go @@ -1,7 +1,9 @@ package controller import ( + "fmt" "log" + "net/http" "time" jwt "github.com/appleboy/gin-jwt/v2" @@ -15,6 +17,7 @@ func initParams() *jwt.GinJWTMiddleware { return &jwt.GinJWTMiddleware{ Realm: singleton.Conf.SiteName, Key: []byte(singleton.Conf.SecretKey), + CookieName: "nz-jwt", Timeout: time.Hour, MaxRefresh: time.Hour, IdentityKey: model.CtxKeyAuthorizedUser, @@ -24,9 +27,20 @@ func initParams() *jwt.GinJWTMiddleware { Authenticator: authenticator(), Authorizator: authorizator(), Unauthorized: unauthorized(), - TokenLookup: "header: Authorization, query: token, cookie: jwt", + TokenLookup: "header: Authorization, query: token, cookie: nz-jwt", TokenHeadName: "Bearer", TimeFunc: time.Now, + + LoginResponse: func(c *gin.Context, code int, token string, expire time.Time) { + c.JSON(http.StatusOK, model.CommonResponse[model.LoginResponse]{ + Success: true, + Data: model.LoginResponse{ + Token: token, + Expire: expire.Format(time.RFC3339), + }, + }) + }, + RefreshResponse: refreshResponse, } } @@ -41,9 +55,9 @@ func handlerMiddleWare(authMiddleware *jwt.GinJWTMiddleware) gin.HandlerFunc { func payloadFunc() func(data interface{}) jwt.MapClaims { return func(data interface{}) jwt.MapClaims { - if v, ok := data.(*model.User); ok { + if v, ok := data.(string); ok { return jwt.MapClaims{ - model.CtxKeyAuthorizedUser: v.ID, + model.CtxKeyAuthorizedUser: v, } } return jwt.MapClaims{} @@ -53,7 +67,7 @@ func payloadFunc() func(data interface{}) jwt.MapClaims { func identityHandler() func(c *gin.Context) interface{} { return func(c *gin.Context) interface{} { claims := jwt.ExtractClaims(c) - userId := claims[model.CtxKeyAuthorizedUser].(uint64) + userId := claims[model.CtxKeyAuthorizedUser].(string) var user model.User if err := singleton.DB.First(&user, userId).Error; err != nil { return nil @@ -62,15 +76,15 @@ func identityHandler() func(c *gin.Context) interface{} { } } -// login test godoc -// @Summary ping example +// User Login +// @Summary user login // @Schemes -// @Description do ping -// @Tags example +// @Description user login // @Accept json +// @param request body model.LoginRequest true "Login Request" // @Produce json -// @Success 200 {string} Helloworld -// @Router /example/login [get] +// @Success 200 {object} model.CommonResponse[model.LoginResponse] +// @Router /login [post] func authenticator() func(c *gin.Context) (interface{}, error) { return func(c *gin.Context) (interface{}, error) { var loginVals model.LoginRequest @@ -79,7 +93,7 @@ func authenticator() func(c *gin.Context) (interface{}, error) { } var user model.User - if err := singleton.DB.Select("id").Where("username = ?", loginVals.Username).First(&user).Error; err != nil { + if err := singleton.DB.Select("id", "password").Where("username = ?", loginVals.Username).First(&user).Error; err != nil { return nil, jwt.ErrFailedAuthentication } @@ -87,7 +101,11 @@ func authenticator() func(c *gin.Context) (interface{}, error) { return nil, jwt.ErrFailedAuthentication } - return &user, nil + if err := singleton.DB.Model(&user).Update("login_expire", time.Now().Add(time.Hour)).Error; err != nil { + return nil, jwt.ErrFailedAuthentication + } + + return fmt.Sprintf("%d", user.ID), nil } } @@ -100,11 +118,37 @@ func authorizator() func(data interface{}, c *gin.Context) bool { func unauthorized() func(c *gin.Context, code int, message string) { return func(c *gin.Context, code int, message string) { - c.JSON(code, model.CommonResponse{ + c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ Success: false, - Error: model.CommonError{ - Code: model.ApiErrorUnauthorized, - }, + Error: "ApiErrorUnauthorized", }) } } + +// Refresh token +// @Summary Refresh token +// @Security BearerAuth +// @Schemes +// @Description Refresh token +// @Tags auth required +// @Produce json +// @Success 200 {object} model.CommonResponse[model.LoginResponse] +// @Router /refresh_token [get] +func refreshResponse(c *gin.Context, code int, token string, expire time.Time) { + claims := jwt.ExtractClaims(c) + userId := claims[model.CtxKeyAuthorizedUser].(string) + if err := singleton.DB.Model(&model.User{}).Where("id = ?", userId).Update("login_expire", expire).Error; err != nil { + c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ + Success: false, + Error: "ApiErrorUnauthorized", + }) + return + } + c.JSON(http.StatusOK, model.CommonResponse[model.LoginResponse]{ + Success: true, + Data: model.LoginResponse{ + Token: token, + Expire: expire.Format(time.RFC3339), + }, + }) +} diff --git a/cmd/dashboard/main.go b/cmd/dashboard/main.go index 7462745..962c2e9 100644 --- a/cmd/dashboard/main.go +++ b/cmd/dashboard/main.go @@ -78,6 +78,27 @@ func initSystem() { } } +// @title Nezha Monitoring API +// @version 1.0 +// @description Nezha Monitoring API +// @termsOfService http://nezhahq.github.io + +// @contact.name API Support +// @contact.url http://nezhahq.github.io +// @contact.email hi@nai.ba + +// @license.name Apache 2.0 +// @license.url http://www.apache.org/licenses/LICENSE-2.0.html + +// @host localhost:8008 +// @BasePath /api/v1 + +// @securityDefinitions.apikey BearerAuth +// @in header +// @name Authorization + +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ func main() { if dashboardCliParam.Version { fmt.Println(singleton.Version) @@ -103,17 +124,23 @@ func main() { srv := controller.ServeWeb() go dispatchReportInfoTask() - if err := graceful.Graceful(func() error { - return srv.Serve(httpL) - }, func(c context.Context) error { - log.Println("NEZHA>> Graceful::START") - singleton.RecordTransferHourlyUsage() - log.Println("NEZHA>> Graceful::END") - m.Close() - return nil - }); err != nil { - log.Printf("NEZHA>> ERROR: %v", err) - } + + go func() { + if err := graceful.Graceful(func() error { + log.Println("NEZHA>> Dashboard::START", singleton.Conf.ListenPort) + return srv.Serve(httpL) + }, func(c context.Context) error { + log.Println("NEZHA>> Graceful::START") + singleton.RecordTransferHourlyUsage() + log.Println("NEZHA>> Graceful::END") + m.Close() + return nil + }); err != nil { + log.Printf("NEZHA>> ERROR: %v", err) + } + }() + + m.Serve() } func dispatchReportInfoTask() { diff --git a/model/api.go b/model/api.go index a16d332..489e6f9 100644 --- a/model/api.go +++ b/model/api.go @@ -23,17 +23,17 @@ func (r ServiceItemResponse) TotalUptime() float32 { } type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` } -type CommonError struct { - Code int `json:"code"` - Args map[string]string `json:"args"` +type CommonResponse[T any] struct { + Success bool `json:"success,omitempty"` + Data T `json:"data,omitempty"` + Error string `json:"error,omitempty"` } -type CommonResponse struct { - Success bool `json:"success"` - Data interface{} `json:"data"` - Error CommonError `json:"error"` +type LoginResponse struct { + Token string `json:"token,omitempty"` + Expire string `json:"expire,omitempty"` } diff --git a/model/common.go b/model/common.go index a14c264..62f5cd2 100644 --- a/model/common.go +++ b/model/common.go @@ -7,15 +7,12 @@ import ( ) const CtxKeyAuthorizedUser = "ckau" -const CtxKeyViewPasswordVerified = "ckvpv" -const CtxKeyPreferredTheme = "ckpt" -const CacheKeyOauth2State = "p:a:state" type Common struct { - ID uint64 `gorm:"primaryKey"` - CreatedAt time.Time `gorm:"index;<-:create"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index"` + ID uint64 `gorm:"primaryKey" json:"id,omitempty"` + CreatedAt time.Time `gorm:"index;<-:create" json:"created_at,omitempty"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at,omitempty"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"` } type Response struct { diff --git a/model/config.go b/model/config.go index 89aa785..855f898 100644 --- a/model/config.go +++ b/model/config.go @@ -117,6 +117,9 @@ func (c *Config) Read(path string) error { return err } + if c.ListenPort == 0 { + c.ListenPort = 8008 + } if c.Language == "" { c.Language = "zh-CN" } diff --git a/model/user.go b/model/user.go index 30f075c..17288cc 100644 --- a/model/user.go +++ b/model/user.go @@ -2,6 +2,6 @@ package model type User struct { Common - Username string - Password string + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty" gorm:"type:char(72)"` } diff --git a/script/bootstrap.sh b/script/bootstrap.sh index 5123b4c..e87de7e 100755 --- a/script/bootstrap.sh +++ b/script/bootstrap.sh @@ -1,4 +1,4 @@ -swag init -g ./cmd/dashboard/main.go -o ./cmd/dashboard/docs +swag init --pd -d . -g ./cmd/dashboard/main.go -o ./cmd/dashboard/docs protoc --go-grpc_out="require_unimplemented_servers=false:." --go_out="." proto/*.proto rm -rf ../agent/proto cp -r proto ../agent \ No newline at end of file