dev: add ddns create, edit and batch delete api (#444)
This commit is contained in:
parent
cf5408751e
commit
aa0d570b2b
@ -1,6 +1,7 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -55,15 +56,20 @@ func routers(r *gin.Engine) {
|
|||||||
api := r.Group("api/v1")
|
api := r.Group("api/v1")
|
||||||
api.POST("/login", authMiddleware.LoginHandler)
|
api.POST("/login", authMiddleware.LoginHandler)
|
||||||
|
|
||||||
unrequiredAuth := api.Group("", unrquiredAuthMiddleware(authMiddleware))
|
optionalAuth := api.Group("", optionalAuthMiddleware(authMiddleware))
|
||||||
unrequiredAuth.GET("/ws/server", serverStream)
|
optionalAuth.GET("/ws/server", commonHandler[any](serverStream))
|
||||||
unrequiredAuth.GET("/server-group", listServerGroup)
|
optionalAuth.GET("/server-group", commonHandler[[]model.ServerGroup](listServerGroup))
|
||||||
|
optionalAuth.GET("/ddns", listDDNS) // TODO
|
||||||
|
|
||||||
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", 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}
|
// cp := commonPage{r: r}
|
||||||
@ -147,3 +153,45 @@ func recordPath(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
c.Set("MatchedPath", url)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
173
cmd/dashboard/controller/ddns.go
Normal file
173
cmd/dashboard/controller/ddns.go
Normal file
@ -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) {}
|
@ -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) {
|
return func(c *gin.Context) {
|
||||||
claims, err := mw.GetClaimsFromJWT(c)
|
claims, err := mw.GetClaimsFromJWT(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -19,24 +19,16 @@ import (
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} model.CommonResponse[any]
|
// @Success 200 {object} model.CommonResponse[any]
|
||||||
// @Router /server/{id} [patch]
|
// @Router /server/{id} [patch]
|
||||||
func editServer(c *gin.Context) {
|
func editServer(c *gin.Context) error {
|
||||||
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 {
|
||||||
c.JSON(http.StatusOK, model.CommonResponse[interface{}]{
|
return err
|
||||||
Success: false,
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
var sf model.EditServer
|
var sf model.EditServer
|
||||||
var s model.Server
|
var s model.Server
|
||||||
if err := c.ShouldBindJSON(&sf); err != nil {
|
if err := c.ShouldBindJSON(&sf); err != nil {
|
||||||
c.JSON(http.StatusOK, model.CommonResponse[interface{}]{
|
return err
|
||||||
Success: false,
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
s.Name = sf.Name
|
s.Name = sf.Name
|
||||||
s.DisplayIndex = sf.DisplayIndex
|
s.DisplayIndex = sf.DisplayIndex
|
||||||
@ -48,20 +40,12 @@ func editServer(c *gin.Context) {
|
|||||||
s.DDNSProfiles = sf.DDNSProfiles
|
s.DDNSProfiles = sf.DDNSProfiles
|
||||||
ddnsProfilesRaw, err := utils.Json.Marshal(s.DDNSProfiles)
|
ddnsProfilesRaw, err := utils.Json.Marshal(s.DDNSProfiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, model.CommonResponse[interface{}]{
|
return err
|
||||||
Success: false,
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
s.DDNSProfilesRaw = string(ddnsProfilesRaw)
|
s.DDNSProfilesRaw = string(ddnsProfilesRaw)
|
||||||
|
|
||||||
if err := singleton.DB.Save(&s).Error; err != nil {
|
if err := singleton.DB.Save(&s).Error; err != nil {
|
||||||
c.JSON(http.StatusOK, model.CommonResponse[interface{}]{
|
return newGormError("%v", err)
|
||||||
Success: false,
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
singleton.ServerLock.Lock()
|
singleton.ServerLock.Lock()
|
||||||
@ -72,6 +56,7 @@ func editServer(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, model.Response{
|
c.JSON(http.StatusOK, model.Response{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
})
|
})
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch delete server
|
// Batch delete server
|
||||||
@ -85,22 +70,14 @@ func editServer(c *gin.Context) {
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} model.CommonResponse[any]
|
// @Success 200 {object} model.CommonResponse[any]
|
||||||
// @Router /batch-delete/server [post]
|
// @Router /batch-delete/server [post]
|
||||||
func batchDeleteServer(c *gin.Context) {
|
func batchDeleteServer(c *gin.Context) error {
|
||||||
var servers []uint64
|
var servers []uint64
|
||||||
if err := c.ShouldBindJSON(&servers); err != nil {
|
if err := c.ShouldBindJSON(&servers); err != nil {
|
||||||
c.JSON(http.StatusOK, model.CommonResponse[interface{}]{
|
return err
|
||||||
Success: false,
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := singleton.DB.Unscoped().Delete(&model.Server{}, "id in (?)", servers).Error; err != nil {
|
if err := singleton.DB.Unscoped().Delete(&model.Server{}, "id in (?)", servers).Error; err != nil {
|
||||||
c.JSON(http.StatusOK, model.CommonResponse[interface{}]{
|
return newGormError("%v", err)
|
||||||
Success: false,
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
singleton.ServerLock.Lock()
|
singleton.ServerLock.Lock()
|
||||||
@ -127,4 +104,5 @@ func batchDeleteServer(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, model.CommonResponse[interface{}]{
|
c.JSON(http.StatusOK, model.CommonResponse[interface{}]{
|
||||||
Success: true,
|
Success: true,
|
||||||
})
|
})
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/naiba/nezha/model"
|
"github.com/naiba/nezha/model"
|
||||||
"github.com/naiba/nezha/pkg/utils"
|
|
||||||
"github.com/naiba/nezha/service/singleton"
|
"github.com/naiba/nezha/service/singleton"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,18 +18,17 @@ import (
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} model.CommonResponse[[]model.ServerGroup]
|
// @Success 200 {object} model.CommonResponse[[]model.ServerGroup]
|
||||||
// @Router /server-group [get]
|
// @Router /server-group [get]
|
||||||
func listServerGroup(c *gin.Context) {
|
func listServerGroup(c *gin.Context) error {
|
||||||
authorizedUser, has := c.Get(model.CtxKeyAuthorizedUser)
|
authorizedUser, has := c.Get(model.CtxKeyAuthorizedUser)
|
||||||
log.Println("bingo test", authorizedUser, has)
|
log.Println("bingo test", authorizedUser, has)
|
||||||
var sg []model.ServerGroup
|
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]{
|
c.JSON(http.StatusOK, model.CommonResponse[[]model.ServerGroup]{
|
||||||
Success: err == nil,
|
Success: true,
|
||||||
Data: sg,
|
Data: sg,
|
||||||
Error: utils.IfOrFn(err == nil, func() string {
|
|
||||||
return err.Error()
|
|
||||||
}, func() string {
|
|
||||||
return ""
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,6 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -27,14 +26,10 @@ var upgrader = websocket.Upgrader{
|
|||||||
// @Produce json
|
// @Produce json
|
||||||
// @Success 200 {object} model.StreamServerData
|
// @Success 200 {object} model.StreamServerData
|
||||||
// @Router /ws/server [get]
|
// @Router /ws/server [get]
|
||||||
func serverStream(c *gin.Context) {
|
func serverStream(c *gin.Context) error {
|
||||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, model.CommonResponse[interface{}]{
|
return err
|
||||||
Success: false,
|
|
||||||
Error: err.Error(),
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
count := 0
|
count := 0
|
||||||
@ -55,6 +50,7 @@ func serverStream(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
time.Sleep(time.Second * 2)
|
time.Sleep(time.Second * 2)
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestGroup singleflight.Group
|
var requestGroup singleflight.Group
|
||||||
|
@ -98,3 +98,20 @@ type DDNSProvider struct {
|
|||||||
WebhookRequestBody bool
|
WebhookRequestBody bool
|
||||||
WebhookHeaders 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
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user