- 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
152 lines
5.7 KiB
Go
152 lines
5.7 KiB
Go
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
|
||
}
|