Maksim Totmin f25f8e4d01 feat: auto-trunk creation, SIP gateway in metrics, gateway WS broadcast
- SIPGateway field in NodeMetric model
- HasNode() engine method
- OnNewNode callback in NATS subscriber
- Auto-create balance trunk from new node metric
- BroadcastGatewayEvent on WS hub
- Gateway status indicators in trunks UI (green/red/gray)
- SPA fallback for frontend routing
2026-06-25 16:56:49 +07:00

152 lines
5.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"encoding/json"
"fmt"
"net/http"
"github.com/pulse-lets-go/internal/config"
"github.com/pulse-lets-go/internal/engine"
"github.com/pulse-lets-go/internal/esl"
"github.com/pulse-lets-go/internal/models"
)
// API — главная структура HTTP API, агрегирует все зависимости.
type API struct {
engine *engine.Engine
configManager *config.Manager
jwtSecret string
monitoringAPIKey string
wsHub *wsHub
natsConnected func() bool // колбэк для проверки NATS-статуса (health)
eslClient *esl.Client // ESL-клиент (nil если не сконфигурирован)
logFormat string
routeLimiter *rateLimiter
apiLimiter *rateLimiter
}
// NewAPI создаёт новый HTTP API с заданными зависимостями.
func NewAPI(eng *engine.Engine, cfgMgr *config.Manager, jwtSecret, monitoringAPIKey string, natsFn func() bool, cfg *config.Config) *API {
a := &API{
engine: eng,
configManager: cfgMgr,
jwtSecret: jwtSecret,
monitoringAPIKey: monitoringAPIKey,
wsHub: newWSHub(),
natsConnected: natsFn,
logFormat: cfg.Log.Format,
}
if cfg.RateLimit.Enabled {
a.routeLimiter = newRateLimiter(cfg.RateLimit.RoutePerSec, cfg.RateLimit.RoutePerSec)
a.apiLimiter = newRateLimiter(cfg.RateLimit.APIPerSec, cfg.RateLimit.APIPerSec)
}
return a
}
// SetESLClient устанавливает ESL-клиент (вызывается после инициализации ESL).
func (a *API) SetESLClient(client *esl.Client) {
a.eslClient = client
}
// Handler возвращает http.Handler со всеми зарегистрированными маршрутами.
func (a *API) Handler() http.Handler {
mux := http.NewServeMux()
// --- Health endpoints (без авторизации) ---
mux.HandleFunc("GET /api/health", a.handleHealth)
mux.HandleFunc("GET /api/health/live", a.handleLiveness)
mux.HandleFunc("GET /api/health/ready", a.handleReadiness)
// --- Публичные маршруты (без авторизации) ---
mux.HandleFunc("POST /api/auth/login", a.handleLogin)
mux.HandleFunc("POST /api/auth/refresh", a.handleRefresh)
mux.HandleFunc("GET /api/route", a.handleRoute)
mux.HandleFunc("GET /ws/metrics", a.handleWS)
// Monitoring с API-key (без JWT)
mux.Handle("GET /api/monitoring/zabbix", a.apiKeyMiddleware(http.HandlerFunc(a.handleZabbix)))
mux.Handle("GET /api/monitoring/prometheus", a.apiKeyMiddleware(http.HandlerFunc(a.handlePrometheus)))
// --- Админка: Nodes (admin + viewer) ---
mux.Handle("GET /api/nodes", a.authMiddleware(http.HandlerFunc(a.handleGetNodes)))
mux.Handle("GET /api/nodes/{id}/metrics", a.authMiddleware(http.HandlerFunc(a.handleGetNodeMetrics)))
mux.Handle("PUT /api/nodes/{id}/toggle", a.authMiddleware(a.adminOnly(http.HandlerFunc(a.handleToggleNode))))
// --- Админка: Trunks (admin only) ---
mux.Handle("GET /api/trunks", a.authMiddleware(a.adminOnly(http.HandlerFunc(a.handleGetTrunks))))
mux.Handle("POST /api/trunks", a.authMiddleware(a.adminOnly(http.HandlerFunc(a.handleCreateTrunk))))
mux.Handle("PUT /api/trunks/{id}", a.authMiddleware(a.adminOnly(http.HandlerFunc(a.handleUpdateTrunk))))
mux.Handle("DELETE /api/trunks/{id}", a.authMiddleware(a.adminOnly(http.HandlerFunc(a.handleDeleteTrunk))))
// --- Админка: Users (admin only) ---
mux.Handle("GET /api/users", a.authMiddleware(a.adminOnly(http.HandlerFunc(a.handleGetUsers))))
mux.Handle("POST /api/users", a.authMiddleware(a.adminOnly(http.HandlerFunc(a.handleCreateUser))))
mux.Handle("PUT /api/users/{id}", a.authMiddleware(a.adminOnly(http.HandlerFunc(a.handleUpdateUser))))
mux.Handle("DELETE /api/users/{id}", a.authMiddleware(a.adminOnly(http.HandlerFunc(a.handleDeleteUser))))
// Логирование + CORS (навешиваем после mux)
var wrapped http.Handler = mux
wrapped = requestLoggingMiddleware(a.logFormat)(wrapped)
// Rate limiting
if a.routeLimiter != nil && a.apiLimiter != nil {
handlerBeforeRL := wrapped
wrapped = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/route" {
rateLimitMiddleware(a.routeLimiter)(handlerBeforeRL).ServeHTTP(w, r)
return
}
if len(r.URL.Path) > 5 && r.URL.Path[:5] == "/api/" {
rateLimitMiddleware(a.apiLimiter)(handlerBeforeRL).ServeHTTP(w, r)
return
}
handlerBeforeRL.ServeHTTP(w, r)
})
}
return corsMiddleware(wrapped)
}
// corsMiddleware добавляет CORS-заголовки.
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
// BroadcastGatewayEvent транслирует статус gateway всем WS-клиентам.
func (a *API) BroadcastGatewayEvent(gatewayName, trunkID, status string) {
msg := &models.WsMetricsMessage{
Type: "gateway_update",
Payload: map[string]string{
"gateway": gatewayName,
"trunk_id": trunkID,
"status": status,
},
}
a.wsHub.broadcast(msg)
}
// decodeJSON декодирует тело запроса в структуру v.
func decodeJSON(r *http.Request, v interface{}) error {
defer r.Body.Close()
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
return fmt.Errorf("декодирование JSON: %w", err)
}
return nil
}