From aa0d570b2b4de7d2b1bdf5967fb411ab0a7a7e46 Mon Sep 17 00:00:00 2001 From: UUBulb <35923940+uubulb@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:30:50 +0800 Subject: [PATCH] dev: add ddns create, edit and batch delete api (#444) --- cmd/dashboard/controller/controller.go | 58 +++++++- cmd/dashboard/controller/ddns.go | 173 +++++++++++++++++++++++ cmd/dashboard/controller/jwt.go | 2 +- cmd/dashboard/controller/server.go | 42 ++---- cmd/dashboard/controller/server_group.go | 16 +-- cmd/dashboard/controller/ws.go | 10 +- model/ddns.go | 17 +++ 7 files changed, 264 insertions(+), 54 deletions(-) create mode 100644 cmd/dashboard/controller/ddns.go diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index 3564299..99f3fc2 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -1,6 +1,7 @@ package controller import ( + "errors" "fmt" "log" "net/http" @@ -55,15 +56,20 @@ func routers(r *gin.Engine) { api := r.Group("api/v1") api.POST("/login", authMiddleware.LoginHandler) - unrequiredAuth := api.Group("", unrquiredAuthMiddleware(authMiddleware)) - unrequiredAuth.GET("/ws/server", serverStream) - unrequiredAuth.GET("/server-group", listServerGroup) + optionalAuth := api.Group("", optionalAuthMiddleware(authMiddleware)) + optionalAuth.GET("/ws/server", commonHandler[any](serverStream)) + optionalAuth.GET("/server-group", commonHandler[[]model.ServerGroup](listServerGroup)) + optionalAuth.GET("/ddns", listDDNS) // TODO auth := api.Group("", authMiddleware.MiddlewareFunc()) auth.GET("/refresh_token", authMiddleware.RefreshHandler) - auth.PATCH("/server/:id", editServer) + auth.PATCH("/server/:id", commonHandler[any](editServer)) - api.DELETE("/batch-delete/server", batchDeleteServer) + auth.POST("/ddns", commonHandler[any](newDDNS)) + auth.PATCH("/ddns/:id", commonHandler[any](editDDNS)) + + api.POST("/batch-delete/server", commonHandler[any](batchDeleteServer)) + api.POST("/batch-delete/ddns", commonHandler[any](batchDeleteDDNS)) // 通用页面 // cp := commonPage{r: r} @@ -147,3 +153,45 @@ func recordPath(c *gin.Context) { } c.Set("MatchedPath", url) } + +func newErrorResponse[T any](err error) model.CommonResponse[T] { + return model.CommonResponse[T]{ + Success: false, + Error: err.Error(), + } +} + +type handlerFunc func(c *gin.Context) error + +// There are many error types in gorm, so create a custom type to represent all +// gorm errors here instead +type gormError struct { + msg string + a []interface{} +} + +func newGormError(format string, args ...interface{}) error { + return &gormError{ + msg: format, + a: args, + } +} + +func (ge *gormError) Error() string { + return fmt.Sprintf(ge.msg, ge.a...) +} + +func commonHandler[T any](handler handlerFunc) func(*gin.Context) { + return func(c *gin.Context) { + if err := handler(c); err != nil { + if _, ok := err.(*gormError); ok { + log.Printf("NEZHA>> gorm error: %v", err) + c.JSON(http.StatusOK, newErrorResponse[T](errors.New("database error"))) + return + } else { + c.JSON(http.StatusOK, newErrorResponse[T](err)) + return + } + } + } +} diff --git a/cmd/dashboard/controller/ddns.go b/cmd/dashboard/controller/ddns.go new file mode 100644 index 0000000..22c217b --- /dev/null +++ b/cmd/dashboard/controller/ddns.go @@ -0,0 +1,173 @@ +package controller + +import ( + "errors" + "fmt" + "net/http" + "strconv" + "strings" + + "github.com/gin-gonic/gin" + "github.com/naiba/nezha/model" + "github.com/naiba/nezha/service/singleton" + "golang.org/x/net/idna" +) + +// Add DDNS configuration +// @Summary Add DDNS configuration +// @Security BearerAuth +// @Schemes +// @Description Add DDNS configuration +// @Tags auth required +// @Accept json +// @param request body model.DDNSForm true "DDNS Request" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /ddns [post] +func newDDNS(c *gin.Context) error { + var df model.DDNSForm + var p model.DDNSProfile + + if err := c.ShouldBindJSON(&df); err != nil { + return err + } + + if df.MaxRetries < 1 || df.MaxRetries > 10 { + return errors.New("重试次数必须为大于 1 且不超过 10 的整数") + } + + p.Name = df.Name + enableIPv4 := df.EnableIPv4 == "on" + enableIPv6 := df.EnableIPv6 == "on" + p.EnableIPv4 = &enableIPv4 + p.EnableIPv6 = &enableIPv6 + p.MaxRetries = df.MaxRetries + p.Provider = df.Provider + p.DomainsRaw = df.DomainsRaw + p.Domains = strings.Split(p.DomainsRaw, ",") + p.AccessID = df.AccessID + p.AccessSecret = df.AccessSecret + p.WebhookURL = df.WebhookURL + p.WebhookMethod = df.WebhookMethod + p.WebhookRequestType = df.WebhookRequestType + p.WebhookRequestBody = df.WebhookRequestBody + p.WebhookHeaders = df.WebhookHeaders + + for n, domain := range p.Domains { + // IDN to ASCII + domainValid, domainErr := idna.Lookup.ToASCII(domain) + if domainErr != nil { + return fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr) + } + p.Domains[n] = domainValid + } + + if err := singleton.DB.Create(&p).Error; err != nil { + return newGormError("%v", err) + } + + singleton.OnDDNSUpdate() + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusOK, + }) + return nil +} + +// Edit DDNS configuration +// @Summary Edit DDNS configuration +// @Security BearerAuth +// @Schemes +// @Description Edit DDNS configuration +// @Tags auth required +// @Accept json +// @param request body model.DDNSForm true "DDNS Request" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /ddns/{id} [patch] +func editDDNS(c *gin.Context) error { + var df model.DDNSForm + var p model.DDNSProfile + + idStr := c.Param("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err != nil { + return err + } + + if err := c.ShouldBindJSON(&df); err != nil { + return err + } + + if df.MaxRetries < 1 || df.MaxRetries > 10 { + return errors.New("重试次数必须为大于 1 且不超过 10 的整数") + } + + p.Name = df.Name + p.ID = id + enableIPv4 := df.EnableIPv4 == "on" + enableIPv6 := df.EnableIPv6 == "on" + p.EnableIPv4 = &enableIPv4 + p.EnableIPv6 = &enableIPv6 + p.MaxRetries = df.MaxRetries + p.Provider = df.Provider + p.DomainsRaw = df.DomainsRaw + p.Domains = strings.Split(p.DomainsRaw, ",") + p.AccessID = df.AccessID + p.AccessSecret = df.AccessSecret + p.WebhookURL = df.WebhookURL + p.WebhookMethod = df.WebhookMethod + p.WebhookRequestType = df.WebhookRequestType + p.WebhookRequestBody = df.WebhookRequestBody + p.WebhookHeaders = df.WebhookHeaders + + for n, domain := range p.Domains { + // IDN to ASCII + domainValid, domainErr := idna.Lookup.ToASCII(domain) + if domainErr != nil { + return fmt.Errorf("域名 %s 解析错误: %v", domain, domainErr) + } + p.Domains[n] = domainValid + } + + if err = singleton.DB.Save(&p).Error; err != nil { + return newGormError("%v", err) + } + + singleton.OnDDNSUpdate() + c.JSON(http.StatusOK, model.Response{ + Code: http.StatusOK, + }) + return nil +} + +// Batch delete DDNS configurations +// @Summary Batch delete DDNS configurations +// @Security BearerAuth +// @Schemes +// @Description Batch delete DDNS configurations +// @Tags auth required +// @Accept json +// @param request body []uint64 true "id list" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /batch-delete/ddns [post] +func batchDeleteDDNS(c *gin.Context) error { + var ddnsConfigs []uint64 + + if err := c.ShouldBindJSON(&ddnsConfigs); err != nil { + return err + } + + if err := singleton.DB.Unscoped().Delete(&model.DDNSProfile{}, "id in (?)", ddnsConfigs).Error; err != nil { + return newGormError("%v", err) + } + + singleton.OnDDNSUpdate() + c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ + Success: true, + }) + return nil +} + +// TODO +func listDDNS(c *gin.Context) {} diff --git a/cmd/dashboard/controller/jwt.go b/cmd/dashboard/controller/jwt.go index e975b2e..8ba4f62 100644 --- a/cmd/dashboard/controller/jwt.go +++ b/cmd/dashboard/controller/jwt.go @@ -145,7 +145,7 @@ func refreshResponse(c *gin.Context, code int, token string, expire time.Time) { }) } -func unrquiredAuthMiddleware(mw *jwt.GinJWTMiddleware) func(c *gin.Context) { +func optionalAuthMiddleware(mw *jwt.GinJWTMiddleware) func(c *gin.Context) { return func(c *gin.Context) { claims, err := mw.GetClaimsFromJWT(c) if err != nil { diff --git a/cmd/dashboard/controller/server.go b/cmd/dashboard/controller/server.go index daab265..c59b409 100644 --- a/cmd/dashboard/controller/server.go +++ b/cmd/dashboard/controller/server.go @@ -19,24 +19,16 @@ import ( // @Produce json // @Success 200 {object} model.CommonResponse[any] // @Router /server/{id} [patch] -func editServer(c *gin.Context) { +func editServer(c *gin.Context) error { idStr := c.Param("id") id, err := strconv.ParseUint(idStr, 10, 64) if err != nil { - c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ - Success: false, - Error: err.Error(), - }) - return + return err } var sf model.EditServer var s model.Server if err := c.ShouldBindJSON(&sf); err != nil { - c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ - Success: false, - Error: err.Error(), - }) - return + return err } s.Name = sf.Name s.DisplayIndex = sf.DisplayIndex @@ -48,20 +40,12 @@ func editServer(c *gin.Context) { s.DDNSProfiles = sf.DDNSProfiles ddnsProfilesRaw, err := utils.Json.Marshal(s.DDNSProfiles) if err != nil { - c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ - Success: false, - Error: err.Error(), - }) - return + return err } s.DDNSProfilesRaw = string(ddnsProfilesRaw) if err := singleton.DB.Save(&s).Error; err != nil { - c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ - Success: false, - Error: err.Error(), - }) - return + return newGormError("%v", err) } singleton.ServerLock.Lock() @@ -72,6 +56,7 @@ func editServer(c *gin.Context) { c.JSON(http.StatusOK, model.Response{ Code: http.StatusOK, }) + return nil } // Batch delete server @@ -85,22 +70,14 @@ func editServer(c *gin.Context) { // @Produce json // @Success 200 {object} model.CommonResponse[any] // @Router /batch-delete/server [post] -func batchDeleteServer(c *gin.Context) { +func batchDeleteServer(c *gin.Context) error { var servers []uint64 if err := c.ShouldBindJSON(&servers); err != nil { - c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ - Success: false, - Error: err.Error(), - }) - return + return err } if err := singleton.DB.Unscoped().Delete(&model.Server{}, "id in (?)", servers).Error; err != nil { - c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ - Success: false, - Error: err.Error(), - }) - return + return newGormError("%v", err) } singleton.ServerLock.Lock() @@ -127,4 +104,5 @@ func batchDeleteServer(c *gin.Context) { c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ Success: true, }) + return nil } diff --git a/cmd/dashboard/controller/server_group.go b/cmd/dashboard/controller/server_group.go index 4ea70d8..cf0b07d 100644 --- a/cmd/dashboard/controller/server_group.go +++ b/cmd/dashboard/controller/server_group.go @@ -6,7 +6,6 @@ import ( "github.com/gin-gonic/gin" "github.com/naiba/nezha/model" - "github.com/naiba/nezha/pkg/utils" "github.com/naiba/nezha/service/singleton" ) @@ -19,18 +18,17 @@ import ( // @Produce json // @Success 200 {object} model.CommonResponse[[]model.ServerGroup] // @Router /server-group [get] -func listServerGroup(c *gin.Context) { +func listServerGroup(c *gin.Context) error { authorizedUser, has := c.Get(model.CtxKeyAuthorizedUser) log.Println("bingo test", authorizedUser, has) var sg []model.ServerGroup - err := singleton.DB.Find(&sg).Error + if err := singleton.DB.Find(&sg).Error; err != nil { + return err + } + c.JSON(http.StatusOK, model.CommonResponse[[]model.ServerGroup]{ - Success: err == nil, + Success: true, Data: sg, - Error: utils.IfOrFn(err == nil, func() string { - return err.Error() - }, func() string { - return "" - }), }) + return nil } diff --git a/cmd/dashboard/controller/ws.go b/cmd/dashboard/controller/ws.go index 262ce74..16c17d3 100644 --- a/cmd/dashboard/controller/ws.go +++ b/cmd/dashboard/controller/ws.go @@ -2,7 +2,6 @@ package controller import ( "fmt" - "net/http" "time" "github.com/gin-gonic/gin" @@ -27,14 +26,10 @@ var upgrader = websocket.Upgrader{ // @Produce json // @Success 200 {object} model.StreamServerData // @Router /ws/server [get] -func serverStream(c *gin.Context) { +func serverStream(c *gin.Context) error { conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { - c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ - Success: false, - Error: err.Error(), - }) - return + return err } defer conn.Close() count := 0 @@ -55,6 +50,7 @@ func serverStream(c *gin.Context) { } time.Sleep(time.Second * 2) } + return nil } var requestGroup singleflight.Group diff --git a/model/ddns.go b/model/ddns.go index 3451110..613c35d 100644 --- a/model/ddns.go +++ b/model/ddns.go @@ -98,3 +98,20 @@ type DDNSProvider struct { WebhookRequestBody bool WebhookHeaders bool } + +type DDNSForm struct { + ID uint64 + MaxRetries uint64 + EnableIPv4 string + EnableIPv6 string + Name string + Provider uint8 + DomainsRaw string + AccessID string + AccessSecret string + WebhookURL string + WebhookMethod uint8 + WebhookRequestType uint8 + WebhookRequestBody string + WebhookHeaders string +}