first
This commit is contained in:
292
backend/internal/api/server.go
Normal file
292
backend/internal/api/server.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"mqtt_explorer/internal/filters"
|
||||
"mqtt_explorer/internal/metrics"
|
||||
"mqtt_explorer/internal/mqtt"
|
||||
"mqtt_explorer/internal/settings"
|
||||
"mqtt_explorer/internal/storage"
|
||||
"mqtt_explorer/internal/sysinfo"
|
||||
"mqtt_explorer/internal/topics"
|
||||
"mqtt_explorer/internal/ws"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
router *gin.Engine
|
||||
store *storage.Store
|
||||
tree *topics.Tree
|
||||
hub *ws.Hub
|
||||
mqttMgr *mqtt.Manager
|
||||
metrics *metrics.Collector
|
||||
filters *filters.Store
|
||||
settings *settings.Store
|
||||
settingsRuntime *settings.Runtime
|
||||
defaults settings.Settings
|
||||
sysinfo *sysinfo.Store
|
||||
}
|
||||
|
||||
type PublishRequest struct {
|
||||
Topic string `json:"topic"`
|
||||
Payload string `json:"payload"`
|
||||
QOS byte `json:"qos"`
|
||||
Retained bool `json:"retained"`
|
||||
}
|
||||
|
||||
type TestConnectionRequest struct {
|
||||
Broker string `json:"broker"`
|
||||
}
|
||||
|
||||
func NewServer(store *storage.Store, tree *topics.Tree, hub *ws.Hub, mqttMgr *mqtt.Manager, filterStore *filters.Store, settingsStore *settings.Store, settingsRuntime *settings.Runtime, defaults settings.Settings, sysStore *sysinfo.Store, staticDir string) *Server {
|
||||
router := gin.Default()
|
||||
|
||||
s := &Server{
|
||||
router: router,
|
||||
store: store,
|
||||
tree: tree,
|
||||
hub: hub,
|
||||
mqttMgr: mqttMgr,
|
||||
metrics: &metrics.Collector{},
|
||||
filters: filterStore,
|
||||
settings: settingsStore,
|
||||
settingsRuntime: settingsRuntime,
|
||||
defaults: defaults,
|
||||
sysinfo: sysStore,
|
||||
}
|
||||
s.registerRoutes()
|
||||
if staticDir != "" {
|
||||
router.Static("/assets", staticDir+"/assets")
|
||||
router.Static("/themes", staticDir+"/themes")
|
||||
router.Static("/favicon", staticDir+"/favicon")
|
||||
router.StaticFile("/site.webmanifest", staticDir+"/site.webmanifest")
|
||||
router.StaticFile("/favicon.ico", staticDir+"/favicon.ico")
|
||||
router.NoRoute(func(c *gin.Context) {
|
||||
c.File(staticDir + "/index.html")
|
||||
})
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) registerRoutes() {
|
||||
s.router.GET("/api/health", s.health)
|
||||
s.router.GET("/api/stats", s.stats)
|
||||
s.router.GET("/api/metrics", s.metricsHandler)
|
||||
s.router.GET("/api/filters", s.getFilters)
|
||||
s.router.POST("/api/filters", s.updateFilters)
|
||||
s.router.GET("/api/settings", s.getSettings)
|
||||
s.router.POST("/api/settings", s.updateSettings)
|
||||
s.router.GET("/api/sysinfo", s.getSysinfo)
|
||||
s.router.POST("/api/test-connection", s.testConnection)
|
||||
s.router.GET("/api/topics", s.getTopics)
|
||||
s.router.GET("/api/topic/:topic/history", s.getHistory)
|
||||
s.router.POST("/api/topic/:topic/clear-history", s.clearHistory)
|
||||
s.router.POST("/api/history/clear", s.clearAllHistory)
|
||||
s.router.POST("/api/publish", s.publish)
|
||||
s.router.GET("/ws/events", s.wsEvents)
|
||||
}
|
||||
|
||||
func (s *Server) Run(addr string) error {
|
||||
return s.router.Run(addr)
|
||||
}
|
||||
|
||||
func (s *Server) health(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) stats(c *gin.Context) {
|
||||
stats, err := s.store.Stats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
|
||||
func (s *Server) metricsHandler(c *gin.Context) {
|
||||
stats, err := s.store.Stats()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
usage := s.metrics.Snapshot()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"cpuPercent": usage.CPUPercent,
|
||||
"memBytes": usage.MemBytes,
|
||||
"memLimit": usage.MemLimit,
|
||||
"dbBytes": stats.Bytes,
|
||||
"dbSize": stats.Size,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) getFilters(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, s.filters.Snapshot())
|
||||
}
|
||||
|
||||
func (s *Server) updateFilters(c *gin.Context) {
|
||||
var rules []filters.Rule
|
||||
if err := c.ShouldBindJSON(&rules); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "payload invalide"})
|
||||
return
|
||||
}
|
||||
s.filters.Update(rules)
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) getSettings(c *gin.Context) {
|
||||
current, err := s.settings.Load(s.defaults)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, current)
|
||||
}
|
||||
|
||||
func (s *Server) updateSettings(c *gin.Context) {
|
||||
var next settings.Settings
|
||||
if err := c.ShouldBindJSON(&next); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "payload invalide"})
|
||||
return
|
||||
}
|
||||
if err := s.settings.Save(next); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
s.filters.Update(toFilterRules(next.TopicFilters))
|
||||
if s.settingsRuntime != nil {
|
||||
s.settingsRuntime.Update(next)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) getSysinfo(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, s.sysinfo.Snapshot())
|
||||
}
|
||||
|
||||
func toFilterRules(filtersIn []settings.TopicFilter) []filters.Rule {
|
||||
out := make([]filters.Rule, 0, len(filtersIn))
|
||||
for _, entry := range filtersIn {
|
||||
out = append(out, filters.Rule{
|
||||
Topic: entry.Topic,
|
||||
Save: entry.Save,
|
||||
View: entry.View,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Server) testConnection(c *gin.Context) {
|
||||
var req TestConnectionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.Broker == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "broker manquant"})
|
||||
return
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(req.Broker)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "broker invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
host := parsed.Hostname()
|
||||
port := parsed.Port()
|
||||
if port == "" {
|
||||
port = "1883"
|
||||
}
|
||||
|
||||
addr := net.JoinHostPort(host, port)
|
||||
start := time.Now()
|
||||
conn, err := net.DialTimeout("tcp", addr, 3*time.Second)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
_ = conn.Close()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"ok": true,
|
||||
"latency": time.Since(start).Milliseconds(),
|
||||
"endpoint": addr,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) getTopics(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, s.tree.Snapshot())
|
||||
}
|
||||
|
||||
func (s *Server) getHistory(c *gin.Context) {
|
||||
topic := c.Param("topic")
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
||||
from := c.DefaultQuery("from", "")
|
||||
to := c.DefaultQuery("to", "")
|
||||
|
||||
messages, err := s.store.GetHistory(topic, limit, from, to)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, messages)
|
||||
}
|
||||
|
||||
func (s *Server) clearHistory(c *gin.Context) {
|
||||
topic := c.Param("topic")
|
||||
deleted, err := s.store.ClearTopicHistory(topic)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"deleted": deleted})
|
||||
}
|
||||
|
||||
func (s *Server) clearAllHistory(c *gin.Context) {
|
||||
deleted, err := s.store.ClearAllHistory()
|
||||
if err != nil {
|
||||
log.Printf("clear db error: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
log.Printf("clear db ok: deleted=%d", deleted)
|
||||
c.JSON(http.StatusOK, gin.H{"deleted": deleted})
|
||||
}
|
||||
|
||||
func (s *Server) publish(c *gin.Context) {
|
||||
var req PublishRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil || req.Topic == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "requete invalide"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.mqttMgr.Publish(req.Topic, []byte(req.Payload), req.QOS, req.Retained); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) wsEvents(c *gin.Context) {
|
||||
upgrader := websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.hub.Add(conn)
|
||||
|
||||
for {
|
||||
if _, _, err := conn.ReadMessage(); err != nil {
|
||||
s.hub.Remove(conn)
|
||||
_ = conn.Close()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user