6bb47d4b00
- Add full admin REST API: auth, API keys, chains, chain tokens, wallets, orders, RPC nodes, settings, dashboard stats, notifications - Add JWT-based admin authentication and API key auth middleware - Add multi-chain listeners: BSC, ETH, Polygon, Plasma, EVM WebSocket - Add RPC node health check job with automatic failover - Add Telegram notification channel for order events - Add installer for first-run database initialization - Add new DB models: admin_user, api_key, chain, chain_token, rpc_node, settings, notification_channel - Add order statistics aggregation (daily/monthly/by-chain) - Serve built admin SPA from embedded www/ assets - Add comprehensive test coverage for all new features
191 lines
6.2 KiB
Go
191 lines
6.2 KiB
Go
package admin
|
|
|
|
import (
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/assimon/luuu/model/data"
|
|
"github.com/assimon/luuu/model/mdb"
|
|
"github.com/assimon/luuu/task"
|
|
"github.com/labstack/echo/v4"
|
|
)
|
|
|
|
// CreateRpcNodeRequest is the payload for creating an RPC node.
|
|
type CreateRpcNodeRequest struct {
|
|
Network string `json:"network" validate:"required" example:"tron"`
|
|
Url string `json:"url" validate:"required" example:"https://api.trongrid.io"`
|
|
// 连接类型 http=HTTP请求 ws=WebSocket长连接
|
|
Type string `json:"type" validate:"required|in:http,ws" enums:"http,ws" example:"http"`
|
|
Weight int `json:"weight" example:"1"`
|
|
ApiKey string `json:"api_key" example:""`
|
|
Enabled *bool `json:"enabled" example:"true"`
|
|
}
|
|
|
|
// UpdateRpcNodeRequest is the payload for updating an RPC node.
|
|
type UpdateRpcNodeRequest struct {
|
|
Url *string `json:"url" example:"https://api.trongrid.io"`
|
|
Weight *int `json:"weight" example:"1"`
|
|
ApiKey *string `json:"api_key" example:"your-api-key"`
|
|
Enabled *bool `json:"enabled" example:"true"`
|
|
}
|
|
|
|
// ListRpcNodes returns rows optionally filtered by network.
|
|
// @Summary List RPC nodes
|
|
// @Description Returns RPC nodes, optionally filtered by network
|
|
// @Tags Admin RPC Nodes
|
|
// @Security AdminJWT
|
|
// @Produce json
|
|
// @Param network query string false "Network filter"
|
|
// @Success 200 {object} response.ApiResponse{data=[]mdb.RpcNode}
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/rpc-nodes [get]
|
|
func (c *BaseAdminController) ListRpcNodes(ctx echo.Context) error {
|
|
network := strings.ToLower(strings.TrimSpace(ctx.QueryParam("network")))
|
|
rows, err := data.ListRpcNodes(network)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, rows)
|
|
}
|
|
|
|
// CreateRpcNode inserts a row. Status starts as "unknown" until the
|
|
// health-check task runs.
|
|
// @Summary Create RPC node
|
|
// @Description Create a new RPC node
|
|
// @Tags Admin RPC Nodes
|
|
// @Security AdminJWT
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body admin.CreateRpcNodeRequest true "RPC node payload"
|
|
// @Success 200 {object} response.ApiResponse{data=mdb.RpcNode}
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/rpc-nodes [post]
|
|
func (c *BaseAdminController) CreateRpcNode(ctx echo.Context) error {
|
|
req := new(CreateRpcNodeRequest)
|
|
if err := ctx.Bind(req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if err := c.ValidateStruct(ctx, req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
weight := req.Weight
|
|
if weight < 1 {
|
|
weight = 1
|
|
}
|
|
enabled := true
|
|
if req.Enabled != nil {
|
|
enabled = *req.Enabled
|
|
}
|
|
row := &mdb.RpcNode{
|
|
Network: strings.ToLower(strings.TrimSpace(req.Network)),
|
|
Url: strings.TrimSpace(req.Url),
|
|
Type: strings.ToLower(strings.TrimSpace(req.Type)),
|
|
Weight: weight,
|
|
ApiKey: req.ApiKey,
|
|
Enabled: enabled,
|
|
Status: mdb.RpcNodeStatusUnknown,
|
|
LastLatencyMs: -1,
|
|
}
|
|
if err := data.CreateRpcNode(row); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, row)
|
|
}
|
|
|
|
// UpdateRpcNode patches url/weight/api_key/enabled.
|
|
// @Summary Update RPC node
|
|
// @Description Patch RPC node fields
|
|
// @Tags Admin RPC Nodes
|
|
// @Security AdminJWT
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "RPC node ID"
|
|
// @Param request body admin.UpdateRpcNodeRequest true "Fields to update"
|
|
// @Success 200 {object} response.ApiResponse
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/rpc-nodes/{id} [patch]
|
|
func (c *BaseAdminController) UpdateRpcNode(ctx echo.Context) error {
|
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
req := new(UpdateRpcNodeRequest)
|
|
if err := ctx.Bind(req); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
fields := map[string]interface{}{}
|
|
if req.Url != nil {
|
|
fields["url"] = *req.Url
|
|
}
|
|
if req.Weight != nil {
|
|
fields["weight"] = *req.Weight
|
|
}
|
|
if req.ApiKey != nil {
|
|
fields["api_key"] = *req.ApiKey
|
|
}
|
|
if req.Enabled != nil {
|
|
fields["enabled"] = *req.Enabled
|
|
}
|
|
if err := data.UpdateRpcNodeFields(id, fields); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, nil)
|
|
}
|
|
|
|
// DeleteRpcNode soft-deletes the row.
|
|
// @Summary Delete RPC node
|
|
// @Description Soft-delete an RPC node
|
|
// @Tags Admin RPC Nodes
|
|
// @Security AdminJWT
|
|
// @Produce json
|
|
// @Param id path int true "RPC node ID"
|
|
// @Success 200 {object} response.ApiResponse
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/rpc-nodes/{id} [delete]
|
|
func (c *BaseAdminController) DeleteRpcNode(ctx echo.Context) error {
|
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if err := data.DeleteRpcNodeByID(id); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, nil)
|
|
}
|
|
|
|
// HealthCheckRpcNode performs an on-demand probe and writes the result.
|
|
// For HTTP endpoints this is a GET to the URL with a short timeout; for
|
|
// WS we just attempt a TCP-level check via the same HTTP client (the
|
|
// handshake URL resolves the host identically).
|
|
// @Summary Health check RPC node
|
|
// @Description Perform an on-demand health probe on an RPC node
|
|
// @Tags Admin RPC Nodes
|
|
// @Security AdminJWT
|
|
// @Produce json
|
|
// @Param id path int true "RPC node ID"
|
|
// @Success 200 {object} response.ApiResponse{data=admin.RpcHealthCheckResponse}
|
|
// @Failure 400 {object} response.ApiResponse
|
|
// @Router /admin/api/v1/rpc-nodes/{id}/health-check [post]
|
|
func (c *BaseAdminController) HealthCheckRpcNode(ctx echo.Context) error {
|
|
id, err := strconv.ParseUint(ctx.Param("id"), 10, 64)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
row, err := data.GetRpcNodeByID(id)
|
|
if err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
if row.ID == 0 {
|
|
return c.FailJson(ctx, errors.New("node not found"))
|
|
}
|
|
status, latency := task.ProbeNode(row.Url)
|
|
if err := data.UpdateRpcNodeHealth(id, status, latency); err != nil {
|
|
return c.FailJson(ctx, err)
|
|
}
|
|
return c.SucJson(ctx, map[string]interface{}{
|
|
"status": status,
|
|
"last_latency_ms": latency,
|
|
})
|
|
}
|