feat: server group api

This commit is contained in:
naiba 2024-10-21 23:00:51 +08:00
parent 5efd995992
commit 9c08ebe956
9 changed files with 223 additions and 43 deletions

View File

@ -12,7 +12,6 @@ on:
branches: branches:
- master - master
jobs: jobs:
tests: tests:
strategy: strategy:
@ -32,6 +31,7 @@ jobs:
- name: bootstrap - name: bootstrap
run: | run: |
go mod tidy -v
go install github.com/swaggo/swag/cmd/swag@latest go install github.com/swaggo/swag/cmd/swag@latest
swag init --pd -d . -g ./cmd/dashboard/main.go -o ./cmd/dashboard/docs swag init --pd -d . -g ./cmd/dashboard/main.go -o ./cmd/dashboard/docs

View File

@ -57,32 +57,23 @@ func routers(r *gin.Engine) {
api.POST("/login", authMiddleware.LoginHandler) api.POST("/login", authMiddleware.LoginHandler)
optionalAuth := api.Group("", optionalAuthMiddleware(authMiddleware)) optionalAuth := api.Group("", optionalAuthMiddleware(authMiddleware))
optionalAuth.GET("/ws/server", commonHandler[any](serverStream)) optionalAuth.GET("/ws/server", commonHandler(serverStream))
optionalAuth.GET("/server-group", commonHandler[[]model.ServerGroup](listServerGroup)) optionalAuth.GET("/server-group", commonHandler(listServerGroup))
auth := api.Group("", authMiddleware.MiddlewareFunc()) auth := api.Group("", authMiddleware.MiddlewareFunc())
auth.GET("/refresh_token", authMiddleware.RefreshHandler) auth.GET("/refresh_token", authMiddleware.RefreshHandler)
auth.PATCH("/server/:id", commonHandler[any](editServer))
auth.GET("/ddns", commonHandler[[]model.DDNSProfile](listDDNS)) auth.POST("/server-group", commonHandler(newServerGroup))
auth.POST("/ddns", commonHandler[any](newDDNS)) auth.PATCH("/server-group/:id", commonHandler(editServerGroup))
auth.PATCH("/ddns/:id", commonHandler[any](editDDNS)) auth.POST("/batch-delete/server-group", commonHandler(batchDeleteServerGroup))
api.POST("/batch-delete/server", commonHandler[any](batchDeleteServer)) auth.PATCH("/server/:id", commonHandler(editServer))
api.POST("/batch-delete/ddns", commonHandler[any](batchDeleteDDNS)) auth.POST("/batch-delete/server", commonHandler(batchDeleteServer))
// 通用页面 auth.GET("/ddns", commonHandler(listDDNS))
// cp := commonPage{r: r} auth.POST("/ddns", commonHandler(newDDNS))
// cp.serve() auth.PATCH("/ddns/:id", commonHandler(editDDNS))
// // 会员页面 auth.POST("/batch-delete/ddns", commonHandler(batchDeleteDDNS))
// mp := &memberPage{r}
// mp.serve()
// // API
// external := api.Group("api")
// {
// ma := &memberAPI{external}
// ma.serve()
// }
} }
func natGateway(c *gin.Context) { func natGateway(c *gin.Context) {
@ -154,8 +145,8 @@ func recordPath(c *gin.Context) {
c.Set("MatchedPath", url) c.Set("MatchedPath", url)
} }
func newErrorResponse[T any](err error) model.CommonResponse[T] { func newErrorResponse(err error) model.CommonResponse[any] {
return model.CommonResponse[T]{ return model.CommonResponse[any]{
Success: false, Success: false,
Error: err.Error(), Error: err.Error(),
} }
@ -181,15 +172,15 @@ func (ge *gormError) Error() string {
return fmt.Sprintf(ge.msg, ge.a...) 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) { return func(c *gin.Context) {
if err := handler(c); err != nil { if err := handler(c); err != nil {
if _, ok := err.(*gormError); ok { if _, ok := err.(*gormError); ok {
log.Printf("NEZHA>> gorm error: %v", err) 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 return
} else { } else {
c.JSON(http.StatusOK, newErrorResponse[T](err)) c.JSON(http.StatusOK, newErrorResponse(err))
return return
} }
} }

View File

@ -81,20 +81,20 @@ func newDDNS(c *gin.Context) error {
// @Description Edit DDNS profile // @Description Edit DDNS profile
// @Tags auth required // @Tags auth required
// @Accept json // @Accept json
// @param id path string true "Profile ID"
// @param request body model.DDNSForm true "DDNS Request" // @param request body model.DDNSForm true "DDNS Request"
// @Produce json // @Produce json
// @Success 200 {object} model.CommonResponse[any] // @Success 200 {object} model.CommonResponse[any]
// @Router /ddns/{id} [patch] // @Router /ddns/{id} [patch]
func editDDNS(c *gin.Context) error { func editDDNS(c *gin.Context) error {
var df model.DDNSForm
var p model.DDNSProfile
idStr := c.Param("id") idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 64) id, err := strconv.ParseUint(idStr, 10, 64)
if err != nil { if err != nil {
return err return err
} }
var df model.DDNSForm
if err := c.ShouldBindJSON(&df); err != nil { if err := c.ShouldBindJSON(&df); err != nil {
return err return err
} }
@ -103,6 +103,11 @@ func editDDNS(c *gin.Context) error {
return errors.New("重试次数必须为大于 1 且不超过 10 的整数") 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.Name = df.Name
p.ID = id p.ID = id
enableIPv4 := df.EnableIPv4 == "on" enableIPv4 := df.EnableIPv4 == "on"

View File

@ -110,7 +110,7 @@ func authorizator() func(data interface{}, c *gin.Context) bool {
func unauthorized() func(c *gin.Context, code int, message string) { func unauthorized() func(c *gin.Context, code int, message string) {
return 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, Success: false,
Error: "ApiErrorUnauthorized", Error: "ApiErrorUnauthorized",
}) })
@ -130,7 +130,7 @@ func refreshResponse(c *gin.Context, code int, token string, expire time.Time) {
claims := jwt.ExtractClaims(c) claims := jwt.ExtractClaims(c)
userId := claims[model.CtxKeyAuthorizedUser].(string) userId := claims[model.CtxKeyAuthorizedUser].(string)
if err := singleton.DB.Model(&model.User{}).Where("id = ?", userId).Update("login_expire", expire).Error; err != nil { 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, Success: false,
Error: "ApiErrorUnauthorized", Error: "ApiErrorUnauthorized",
}) })

View File

@ -17,6 +17,9 @@ import (
// @Schemes // @Schemes
// @Description Edit server // @Description Edit server
// @Tags auth required // @Tags auth required
// @Accept json
// @Param id path uint true "Server ID"
// @Param body body model.ServerForm true "ServerForm"
// @Produce json // @Produce json
// @Success 200 {object} model.CommonResponse[any] // @Success 200 {object} model.CommonResponse[any]
// @Router /server/{id} [patch] // @Router /server/{id} [patch]
@ -26,11 +29,16 @@ func editServer(c *gin.Context) error {
if err != nil { if err != nil {
return err return err
} }
var sf model.EditServer var sf model.ServerForm
var s model.Server
if err := c.ShouldBindJSON(&sf); err != nil { if err := c.ShouldBindJSON(&sf); err != nil {
return err 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.Name = sf.Name
s.DisplayIndex = sf.DisplayIndex s.DisplayIndex = sf.DisplayIndex
s.ID = id s.ID = id
@ -102,7 +110,7 @@ func batchDeleteServer(c *gin.Context) error {
singleton.ReSortServer() singleton.ReSortServer()
c.JSON(http.StatusOK, model.CommonResponse[interface{}]{ c.JSON(http.StatusOK, model.CommonResponse[any]{
Success: true, Success: true,
}) })
return nil return nil

View File

@ -1,10 +1,11 @@
package controller package controller
import ( import (
"log" "fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/naiba/nezha/model" "github.com/naiba/nezha/model"
"github.com/naiba/nezha/service/singleton" "github.com/naiba/nezha/service/singleton"
@ -17,19 +18,183 @@ import (
// @Security BearerAuth // @Security BearerAuth
// @Tags common // @Tags common
// @Produce json // @Produce json
// @Success 200 {object} model.CommonResponse[[]model.ServerGroup] // @Success 200 {object} model.CommonResponse[[]model.ServerGroupResponseItem]
// @Router /server-group [get] // @Router /server-group [get]
func listServerGroup(c *gin.Context) error { func listServerGroup(c *gin.Context) error {
authorizedUser, has := c.Get(model.CtxKeyAuthorizedUser)
log.Println("bingo test", authorizedUser, has)
var sg []model.ServerGroup var sg []model.ServerGroup
if err := singleton.DB.Find(&sg).Error; err != nil { if err := singleton.DB.Find(&sg).Error; err != nil {
return err 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, Success: true,
Data: sg,
}) })
return nil return nil
} }

View File

@ -18,7 +18,7 @@ type StreamServerData struct {
Servers []StreamServer `json:"servers,omitempty"` Servers []StreamServer `json:"servers,omitempty"`
} }
type EditServer struct { type ServerForm struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Note string `json:"note,omitempty"` // 管理员可见备注 Note string `json:"note,omitempty"` // 管理员可见备注
PublicNote string `json:"public_note,omitempty"` // 公开备注 PublicNote string `json:"public_note,omitempty"` // 公开备注

11
model/server_group_api.go Normal file
View File

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

View File

@ -2,6 +2,6 @@ package model
type ServerGroupServer struct { type ServerGroupServer struct {
Common Common
ServerGroupId uint64 `json:"server_group_id"` ServerGroupId uint64 `json:"server_group_id" gorm:"uniqueIndex:idx_server_group_server"`
ServerId uint64 `json:"server_id"` ServerId uint64 `json:"server_id" gorm:"uniqueIndex:idx_server_group_server"`
} }