mirror of
https://github.com/nezhahq/nezha.git
synced 2025-01-22 12:48:14 -05:00
feat: server group api
This commit is contained in:
parent
5efd995992
commit
9c08ebe956
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -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
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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",
|
||||||
})
|
})
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
11
model/server_group_api.go
Normal 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"`
|
||||||
|
}
|
@ -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"`
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user