2026-06-25 14:27:41 +07:00

138 lines
5.3 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"
)
// 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)
})
}
// 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
}