From 9c08ebe956033c8e7d0dfc260e50bbc5495ca88d Mon Sep 17 00:00:00 2001 From: naiba Date: Mon, 21 Oct 2024 23:00:51 +0800 Subject: [PATCH] feat: server group api --- .github/workflows/test.yml | 2 +- cmd/dashboard/controller/controller.go | 41 ++---- cmd/dashboard/controller/ddns.go | 11 +- cmd/dashboard/controller/jwt.go | 4 +- cmd/dashboard/controller/server.go | 14 +- cmd/dashboard/controller/server_group.go | 177 ++++++++++++++++++++++- model/server_api.go | 2 +- model/server_group_api.go | 11 ++ model/server_group_server.go | 4 +- 9 files changed, 223 insertions(+), 43 deletions(-) create mode 100644 model/server_group_api.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index abd374c..0813d03 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,7 +12,6 @@ on: branches: - master - jobs: tests: strategy: @@ -32,6 +31,7 @@ jobs: - name: bootstrap run: | + go mod tidy -v go install github.com/swaggo/swag/cmd/swag@latest swag init --pd -d . -g ./cmd/dashboard/main.go -o ./cmd/dashboard/docs diff --git a/cmd/dashboard/controller/controller.go b/cmd/dashboard/controller/controller.go index 9c87b01..88f45ce 100644 --- a/cmd/dashboard/controller/controller.go +++ b/cmd/dashboard/controller/controller.go @@ -57,32 +57,23 @@ func routers(r *gin.Engine) { api.POST("/login", authMiddleware.LoginHandler) optionalAuth := api.Group("", optionalAuthMiddleware(authMiddleware)) - optionalAuth.GET("/ws/server", commonHandler[any](serverStream)) - optionalAuth.GET("/server-group", commonHandler[[]model.ServerGroup](listServerGroup)) + optionalAuth.GET("/ws/server", commonHandler(serverStream)) + optionalAuth.GET("/server-group", commonHandler(listServerGroup)) auth := api.Group("", authMiddleware.MiddlewareFunc()) auth.GET("/refresh_token", authMiddleware.RefreshHandler) - auth.PATCH("/server/:id", commonHandler[any](editServer)) - auth.GET("/ddns", commonHandler[[]model.DDNSProfile](listDDNS)) - auth.POST("/ddns", commonHandler[any](newDDNS)) - auth.PATCH("/ddns/:id", commonHandler[any](editDDNS)) + auth.POST("/server-group", commonHandler(newServerGroup)) + auth.PATCH("/server-group/:id", commonHandler(editServerGroup)) + auth.POST("/batch-delete/server-group", commonHandler(batchDeleteServerGroup)) - api.POST("/batch-delete/server", commonHandler[any](batchDeleteServer)) - api.POST("/batch-delete/ddns", commonHandler[any](batchDeleteDDNS)) + auth.PATCH("/server/:id", commonHandler(editServer)) + auth.POST("/batch-delete/server", commonHandler(batchDeleteServer)) - // 通用页面 - // cp := commonPage{r: r} - // cp.serve() - // // 会员页面 - // mp := &memberPage{r} - // mp.serve() - // // API - // external := api.Group("api") - // { - // ma := &memberAPI{external} - // ma.serve() - // } + auth.GET("/ddns", commonHandler(listDDNS)) + auth.POST("/ddns", commonHandler(newDDNS)) + auth.PATCH("/ddns/:id", commonHandler(editDDNS)) + auth.POST("/batch-delete/ddns", commonHandler(batchDeleteDDNS)) } func natGateway(c *gin.Context) { @@ -154,8 +145,8 @@ func recordPath(c *gin.Context) { c.Set("MatchedPath", url) } -func newErrorResponse[T any](err error) model.CommonResponse[T] { - return model.CommonResponse[T]{ +func newErrorResponse(err error) model.CommonResponse[any] { + return model.CommonResponse[any]{ Success: false, Error: err.Error(), } @@ -181,15 +172,15 @@ func (ge *gormError) Error() string { return fmt.Sprintf(ge.msg, ge.a...) } -func commonHandler[T any](handler handlerFunc) func(*gin.Context) { +func commonHandler(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"))) + c.JSON(http.StatusOK, newErrorResponse(errors.New("database error"))) return } else { - c.JSON(http.StatusOK, newErrorResponse[T](err)) + c.JSON(http.StatusOK, newErrorResponse(err)) return } } diff --git a/cmd/dashboard/controller/ddns.go b/cmd/dashboard/controller/ddns.go index ddc0e93..479e9c4 100644 --- a/cmd/dashboard/controller/ddns.go +++ b/cmd/dashboard/controller/ddns.go @@ -81,20 +81,20 @@ func newDDNS(c *gin.Context) error { // @Description Edit DDNS profile // @Tags auth required // @Accept json +// @param id path string true "Profile ID" // @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 } + var df model.DDNSForm if err := c.ShouldBindJSON(&df); err != nil { return err } @@ -103,6 +103,11 @@ func editDDNS(c *gin.Context) error { return errors.New("重试次数必须为大于 1 且不超过 10 的整数") } + var p model.DDNSProfile + if err = singleton.DB.First(&p, id).Error; err != nil { + return newGormError("%v", err) + } + p.Name = df.Name p.ID = id enableIPv4 := df.EnableIPv4 == "on" diff --git a/cmd/dashboard/controller/jwt.go b/cmd/dashboard/controller/jwt.go index 8ba4f62..cef417d 100644 --- a/cmd/dashboard/controller/jwt.go +++ b/cmd/dashboard/controller/jwt.go @@ -110,7 +110,7 @@ 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(http.StatusOK, model.CommonResponse[interface{}]{ + c.JSON(http.StatusOK, model.CommonResponse[any]{ Success: false, Error: "ApiErrorUnauthorized", }) @@ -130,7 +130,7 @@ 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{}]{ + c.JSON(http.StatusOK, model.CommonResponse[any]{ Success: false, Error: "ApiErrorUnauthorized", }) diff --git a/cmd/dashboard/controller/server.go b/cmd/dashboard/controller/server.go index ba46c19..fd0d0ce 100644 --- a/cmd/dashboard/controller/server.go +++ b/cmd/dashboard/controller/server.go @@ -17,6 +17,9 @@ import ( // @Schemes // @Description Edit server // @Tags auth required +// @Accept json +// @Param id path uint true "Server ID" +// @Param body body model.ServerForm true "ServerForm" // @Produce json // @Success 200 {object} model.CommonResponse[any] // @Router /server/{id} [patch] @@ -26,11 +29,16 @@ func editServer(c *gin.Context) error { if err != nil { return err } - var sf model.EditServer - var s model.Server + var sf model.ServerForm if err := c.ShouldBindJSON(&sf); err != nil { return err } + + var s model.Server + if err := singleton.DB.First(&s, id).Error; err != nil { + return newGormError("%v", err) + } + s.Name = sf.Name s.DisplayIndex = sf.DisplayIndex s.ID = id @@ -102,7 +110,7 @@ func batchDeleteServer(c *gin.Context) error { singleton.ReSortServer() - c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ + c.JSON(http.StatusOK, model.CommonResponse[any]{ Success: true, }) return nil diff --git a/cmd/dashboard/controller/server_group.go b/cmd/dashboard/controller/server_group.go index 1d5ac12..358ee0d 100644 --- a/cmd/dashboard/controller/server_group.go +++ b/cmd/dashboard/controller/server_group.go @@ -1,10 +1,11 @@ package controller import ( - "log" + "fmt" "net/http" "github.com/gin-gonic/gin" + "gorm.io/gorm" "github.com/naiba/nezha/model" "github.com/naiba/nezha/service/singleton" @@ -17,19 +18,183 @@ import ( // @Security BearerAuth // @Tags common // @Produce json -// @Success 200 {object} model.CommonResponse[[]model.ServerGroup] +// @Success 200 {object} model.CommonResponse[[]model.ServerGroupResponseItem] // @Router /server-group [get] func listServerGroup(c *gin.Context) error { - authorizedUser, has := c.Get(model.CtxKeyAuthorizedUser) - log.Println("bingo test", authorizedUser, has) var sg []model.ServerGroup if err := singleton.DB.Find(&sg).Error; err != nil { return err } - c.JSON(http.StatusOK, model.CommonResponse[[]model.ServerGroup]{ + groupServers := make(map[uint64][]uint64, 0) + var sgs []model.ServerGroupServer + if err := singleton.DB.Find(&sgs).Error; err != nil { + return err + } + for _, s := range sgs { + if _, ok := groupServers[s.ServerGroupId]; !ok { + groupServers[s.ServerGroupId] = make([]uint64, 0) + } + groupServers[s.ServerGroupId] = append(groupServers[s.ServerGroupId], s.ServerId) + } + + var sgRes []model.ServerGroupResponseItem + for _, s := range sg { + sgRes = append(sgRes, model.ServerGroupResponseItem{ + Group: s, + Servers: groupServers[s.ID], + }) + } + + c.JSON(http.StatusOK, model.CommonResponse[[]model.ServerGroupResponseItem]{ + Success: true, + Data: sgRes, + }) + return nil +} + +// New server group +// @Summary New server group +// @Schemes +// @Description New server group +// @Security BearerAuth +// @Tags auth required +// @Accept json +// @Param body body model.ServerGroupForm true "ServerGroupForm" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /server-group [post] +func newServerGroup(c *gin.Context) error { + var sgf model.ServerGroupForm + if err := c.ShouldBindJSON(&sgf); err != nil { + return err + } + + var sg model.ServerGroup + sg.Name = sgf.Name + + var count int64 + if err := singleton.DB.Model(&model.Server{}).Where("id = ?", sgf.Servers).Count(&count).Error; err != nil { + return err + } + if count != int64(len(sgf.Servers)) { + return fmt.Errorf("have invalid server id") + } + + singleton.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Create(&sg).Error; err != nil { + return err + } + for _, s := range sgf.Servers { + if err := tx.Create(&model.ServerGroupServer{ + ServerGroupId: sg.ID, + ServerId: s, + }).Error; err != nil { + return err + } + } + return nil + }) + + c.JSON(http.StatusOK, model.CommonResponse[any]{ + Success: true, + }) + return nil +} + +// Edit server group +// @Summary Edit server group +// @Schemes +// @Description Edit server group +// @Security BearerAuth +// @Tags auth required +// @Accept json +// @Param id path string true "ID" +// @Param body body model.ServerGroupForm true "ServerGroupForm" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /server-group/{id} [put] +func editServerGroup(c *gin.Context) error { + id := c.Param("id") + var sg model.ServerGroupForm + if err := c.ShouldBindJSON(&sg); err != nil { + return err + } + var sgDB model.ServerGroup + if err := singleton.DB.First(&sgDB, id).Error; err != nil { + return newGormError("%v", err) + } + sgDB.Name = sg.Name + + var count int64 + if err := singleton.DB.Model(&model.Server{}).Where("id = ?", sg.Servers).Count(&count).Error; err != nil { + return err + } + if count != int64(len(sg.Servers)) { + return fmt.Errorf("have invalid server id") + } + + err := singleton.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Save(&sgDB).Error; err != nil { + return err + } + if err := tx.Delete(&model.ServerGroupServer{}, "server_group_id = ?", id).Error; err != nil { + return err + } + + for _, s := range sg.Servers { + if err := tx.Create(&model.ServerGroupServer{ + ServerGroupId: sgDB.ID, + ServerId: s, + }).Error; err != nil { + return err + } + } + return nil + }) + if err != nil { + return newGormError("%v", err) + } + + c.JSON(http.StatusOK, model.CommonResponse[any]{ + Success: true, + }) + return nil +} + +// Batch delete server group +// @Summary Batch delete server group +// @Security BearerAuth +// @Schemes +// @Description Batch delete server group +// @Tags auth required +// @Accept json +// @param request body []uint64 true "id list" +// @Produce json +// @Success 200 {object} model.CommonResponse[any] +// @Router /batch-delete/server-group [post] +func batchDeleteServerGroup(c *gin.Context) error { + var sgs []uint64 + if err := c.ShouldBindJSON(&sgs); err != nil { + return err + } + + err := singleton.DB.Transaction(func(tx *gorm.DB) error { + if err := tx.Unscoped().Delete(&model.ServerGroup{}, "id in (?)", sgs).Error; err != nil { + return err + } + if err := tx.Unscoped().Delete(&model.ServerGroupServer{}, "server_group_id in (?)", sgs).Error; err != nil { + return err + } + return nil + }) + + if err != nil { + return newGormError("%v", err) + } + + c.JSON(http.StatusOK, model.CommonResponse[any]{ Success: true, - Data: sg, }) return nil } diff --git a/model/server_api.go b/model/server_api.go index e0b8fc9..3b9d7a6 100644 --- a/model/server_api.go +++ b/model/server_api.go @@ -18,7 +18,7 @@ type StreamServerData struct { Servers []StreamServer `json:"servers,omitempty"` } -type EditServer struct { +type ServerForm struct { Name string `json:"name,omitempty"` Note string `json:"note,omitempty"` // 管理员可见备注 PublicNote string `json:"public_note,omitempty"` // 公开备注 diff --git a/model/server_group_api.go b/model/server_group_api.go new file mode 100644 index 0000000..591b5d2 --- /dev/null +++ b/model/server_group_api.go @@ -0,0 +1,11 @@ +package model + +type ServerGroupForm struct { + Name string `json:"name"` + Servers []uint64 `json:"servers"` +} + +type ServerGroupResponseItem struct { + Group ServerGroup `json:"group"` + Servers []uint64 `json:"servers"` +} diff --git a/model/server_group_server.go b/model/server_group_server.go index 2786431..7bf0ebb 100644 --- a/model/server_group_server.go +++ b/model/server_group_server.go @@ -2,6 +2,6 @@ package model type ServerGroupServer struct { Common - ServerGroupId uint64 `json:"server_group_id"` - ServerId uint64 `json:"server_id"` + ServerGroupId uint64 `json:"server_group_id" gorm:"uniqueIndex:idx_server_group_server"` + ServerId uint64 `json:"server_id" gorm:"uniqueIndex:idx_server_group_server"` }