mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-21 04:04:28 +00:00
feat: Add comprehensive API server with REST endpoints, WebSocket support, RBAC, monitoring, and service integrations
- Add server.go with Swagger documentation endpoint, WebSocket handler, health/ready/live monitoring endpoints - Add global middleware: rate limiting, performance optimization, security headers, CORS, request size (10MB), timeout (30s) - Add performance metrics endpoint with 100ms threshold monitoring - Add authentication service initialization with JWT, WebSocket hub, health monitor
This commit is contained in:
@@ -16,8 +16,16 @@ import (
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/config"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/database"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/graphql"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/handlers"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/metrics"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/middleware"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/monitoring"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/rbac"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/services"
|
||||
"github.com/nutcas3/telecom-platform/apps/api-server/internal/websocket"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
ginSwagger "github.com/swaggo/gin-swagger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -94,6 +102,281 @@ func main() {
|
||||
})
|
||||
})
|
||||
|
||||
// Add Swagger documentation endpoints
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
|
||||
// Add WebSocket endpoint
|
||||
router.GET("/ws", websocket.HandleWebSocket(websocket.GetHub()))
|
||||
|
||||
// Add health check endpoints
|
||||
router.GET("/health", monitoring.HealthHandler())
|
||||
router.GET("/ready", monitoring.ReadyHandler())
|
||||
router.GET("/live", monitoring.LiveHandler())
|
||||
|
||||
// Apply global rate limiting
|
||||
router.Use(middleware.RateLimitByEndpoint())
|
||||
|
||||
// Apply performance optimization middleware
|
||||
router.Use(middleware.PerformanceMiddlewareHandler())
|
||||
router.Use(middleware.SecurityHeadersMiddleware())
|
||||
router.Use(middleware.CORSMiddleware())
|
||||
router.Use(middleware.RequestSizeMiddleware(10 << 20)) // 10MB limit
|
||||
router.Use(middleware.TimeoutMiddleware(30 * time.Second))
|
||||
|
||||
// Add performance metrics endpoint
|
||||
router.GET("/metrics/performance", middleware.PerformanceHandler())
|
||||
|
||||
// Initialize authentication service
|
||||
authSvc := services.NewAuthService(db.DB, cfg.Auth.JWTSecret)
|
||||
|
||||
// Initialize WebSocket hub
|
||||
websocket.InitializeWebSocket()
|
||||
|
||||
// Initialize health monitor
|
||||
monitoring.InitializeHealthMonitor("1.0.0", "production")
|
||||
|
||||
// Register health checks
|
||||
healthMonitor := monitoring.GetHealthMonitor()
|
||||
healthMonitor.RegisterCheck("database", monitoring.NewDatabaseHealthChecker(db.DB))
|
||||
|
||||
// Register system health check
|
||||
healthMonitor.RegisterCheck("system", monitoring.NewSystemHealthChecker())
|
||||
|
||||
// Initialize alert manager
|
||||
monitoring.InitializeAlertManager(healthMonitor)
|
||||
|
||||
// Initialize performance middleware
|
||||
middleware.InitializePerformanceMiddleware(100 * time.Millisecond) // 100ms threshold
|
||||
|
||||
// Initialize Casbin RBAC service
|
||||
casbinSvc, err := rbac.NewCasbinService(db.DB)
|
||||
if err != nil {
|
||||
log.Printf("Failed to initialize Casbin service: %v", err)
|
||||
// Continue without Casbin - fallback to basic role checking
|
||||
casbinSvc = nil
|
||||
}
|
||||
|
||||
// Initialize real, DB/K8s/Prometheus-backed services.
|
||||
chaosSvc := services.NewChaosService(db)
|
||||
invoiceSvc := services.NewInvoiceService(db)
|
||||
pluginSvc := services.NewPluginService(db)
|
||||
automationSvc := services.NewAutomationService(db)
|
||||
configStoreSvc := services.NewConfigStoreService(db)
|
||||
deploymentSvc := services.NewDeploymentService(db)
|
||||
promSvc := monitoring.NewPrometheusService()
|
||||
|
||||
// Kubernetes service is optional: if not configured we surface a clear
|
||||
// 503 from service management endpoints rather than returning fake data.
|
||||
k8sSvc, k8sErr := services.NewKubernetesService()
|
||||
if k8sErr != nil {
|
||||
log.Printf("Kubernetes integration disabled: %v", k8sErr)
|
||||
}
|
||||
|
||||
// Build handlers wired to real services.
|
||||
authH := handlers.NewAuthHandler(authSvc)
|
||||
servicesH := handlers.NewServicesHandler(k8sSvc)
|
||||
monitoringH := handlers.NewMonitoringHandler(promSvc)
|
||||
deploymentsH := handlers.NewDeploymentsHandler(deploymentSvc)
|
||||
pluginsH := handlers.NewPluginsHandler(pluginSvc)
|
||||
automationH := handlers.NewAutomationHandler(automationSvc)
|
||||
configH := handlers.NewConfigHandler(configStoreSvc)
|
||||
chaosH := handlers.NewChaosHandler(chaosSvc)
|
||||
billingH := handlers.NewBillingHandler(invoiceSvc, db.DB)
|
||||
|
||||
// API v1 routes
|
||||
v1 := router.Group("/v1")
|
||||
{
|
||||
// Authentication routes (no auth required)
|
||||
auth := v1.Group("/auth")
|
||||
{
|
||||
auth.POST("/login", authH.Login)
|
||||
auth.POST("/register", authH.Register)
|
||||
auth.POST("/refresh", authH.RefreshToken)
|
||||
}
|
||||
|
||||
// Protected routes (authentication required)
|
||||
protected := v1.Group("/")
|
||||
protected.Use(middleware.AuthMiddleware(authSvc))
|
||||
{
|
||||
// User management
|
||||
authProtected := protected.Group("/auth")
|
||||
{
|
||||
authProtected.POST("/logout", authH.Logout)
|
||||
authProtected.GET("/profile", authH.GetProfile)
|
||||
authProtected.POST("/change-password", authH.ChangePassword)
|
||||
}
|
||||
|
||||
// Admin-only user management
|
||||
users := protected.Group("/users")
|
||||
if casbinSvc != nil {
|
||||
users.Use(middleware.RequireCasbinPermission(casbinSvc, "/v1/users", "GET"))
|
||||
} else {
|
||||
users.Use(middleware.RequireRole("admin"))
|
||||
}
|
||||
{
|
||||
users.GET("", authH.GetUsers)
|
||||
users.POST("", authH.CreateUser)
|
||||
users.PUT("/:id", authH.UpdateUser)
|
||||
users.DELETE("/:id", authH.DeleteUser)
|
||||
}
|
||||
}
|
||||
|
||||
// Protected API endpoints (authentication required)
|
||||
apiProtected := v1.Group("/")
|
||||
apiProtected.Use(middleware.AuthMiddleware(authSvc))
|
||||
{
|
||||
// Services Management
|
||||
svcs := apiProtected.Group("/services")
|
||||
{
|
||||
svcs.GET("", servicesH.List)
|
||||
svcs.GET("/:id", servicesH.Get)
|
||||
svcs.GET("/:id/logs", servicesH.Logs)
|
||||
svcs.GET("/:id/health", servicesH.Health)
|
||||
svcs.GET("/:id/pods", servicesH.PodStatus)
|
||||
svcs.GET("/:id/events", servicesH.Events)
|
||||
|
||||
// Write operations require higher permissions
|
||||
svcsWrite := svcs.Group("/")
|
||||
if casbinSvc != nil {
|
||||
svcsWrite.Use(middleware.RequireCasbinPermission(casbinSvc, "/v1/services", "POST"))
|
||||
} else {
|
||||
svcsWrite.Use(middleware.RequireRole("admin", "operator"))
|
||||
}
|
||||
{
|
||||
svcsWrite.POST("/:id/restart", servicesH.Restart)
|
||||
svcsWrite.POST("/:id/start", servicesH.Start)
|
||||
svcsWrite.POST("/:id/stop", servicesH.Stop)
|
||||
svcsWrite.POST("/:id/scale", servicesH.Scale)
|
||||
}
|
||||
}
|
||||
|
||||
// Monitoring & Metrics (read-only for most roles)
|
||||
mon := apiProtected.Group("/monitoring")
|
||||
{
|
||||
mon.GET("/metrics", monitoringH.Metrics)
|
||||
mon.GET("/alerts", monitoringH.Alerts)
|
||||
mon.GET("/health", monitoringH.Health)
|
||||
mon.GET("/logs", monitoringH.Logs)
|
||||
}
|
||||
|
||||
// Deployment Management
|
||||
deploy := apiProtected.Group("/deploy")
|
||||
{
|
||||
deploy.GET("/status", deploymentsH.Status)
|
||||
deploy.GET("/history", deploymentsH.History)
|
||||
|
||||
// Write operations require higher permissions
|
||||
deployWrite := deploy.Group("/")
|
||||
if casbinSvc != nil {
|
||||
deployWrite.Use(middleware.RequireCasbinPermission(casbinSvc, "/v1/deploy", "POST"))
|
||||
} else {
|
||||
deployWrite.Use(middleware.RequireRole("admin", "operator"))
|
||||
}
|
||||
{
|
||||
deployWrite.POST("/start", deploymentsH.Start)
|
||||
deployWrite.POST("/rollback", deploymentsH.Rollback)
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin Management
|
||||
plugs := apiProtected.Group("/plugins")
|
||||
{
|
||||
plugs.GET("", pluginsH.List)
|
||||
|
||||
// Write operations require admin permissions
|
||||
plugsWrite := plugs.Group("/")
|
||||
if casbinSvc != nil {
|
||||
plugsWrite.Use(middleware.RequireCasbinPermission(casbinSvc, "/v1/plugins", "POST"))
|
||||
plugsDelete := plugs.Group("/")
|
||||
plugsDelete.Use(middleware.RequireCasbinPermission(casbinSvc, "/v1/plugins", "DELETE"))
|
||||
plugsDelete.DELETE("/:id", pluginsH.Uninstall)
|
||||
} else {
|
||||
plugsWrite.Use(middleware.RequireRole("admin"))
|
||||
plugsWrite.DELETE("/:id", pluginsH.Uninstall)
|
||||
}
|
||||
{
|
||||
plugsWrite.POST("/install", pluginsH.Install)
|
||||
plugsWrite.POST("/:id/enable", pluginsH.Enable)
|
||||
plugsWrite.POST("/:id/disable", pluginsH.Disable)
|
||||
}
|
||||
}
|
||||
|
||||
// Automation Management
|
||||
auto := apiProtected.Group("/automation")
|
||||
{
|
||||
auto.GET("", automationH.List)
|
||||
auto.GET("/logs", automationH.Logs)
|
||||
|
||||
// Write operations require higher permissions
|
||||
autoWrite := auto.Group("/")
|
||||
if casbinSvc != nil {
|
||||
autoWrite.Use(middleware.RequireCasbinPermission(casbinSvc, "/v1/automation", "POST"))
|
||||
} else {
|
||||
autoWrite.Use(middleware.RequireRole("admin", "operator"))
|
||||
}
|
||||
{
|
||||
autoWrite.POST("", automationH.Create)
|
||||
autoWrite.POST("/:id/run", automationH.Run)
|
||||
autoWrite.POST("/:id/schedule", automationH.Schedule)
|
||||
}
|
||||
}
|
||||
|
||||
// Billing & Invoicing
|
||||
bill := apiProtected.Group("/billing")
|
||||
{
|
||||
bill.GET("/invoices", billingH.ListInvoices)
|
||||
bill.GET("/payments", billingH.ListPayments)
|
||||
|
||||
// Invoice generation requires admin permissions
|
||||
billWrite := bill.Group("/")
|
||||
if casbinSvc != nil {
|
||||
billWrite.Use(middleware.RequireCasbinPermission(casbinSvc, "/v1/billing", "POST"))
|
||||
} else {
|
||||
billWrite.Use(middleware.RequireRole("admin"))
|
||||
}
|
||||
{
|
||||
billWrite.POST("/invoices", billingH.GenerateInvoice)
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration Management
|
||||
cfg := apiProtected.Group("/config")
|
||||
{
|
||||
cfg.GET("", configH.Get)
|
||||
cfg.GET("/validate", configH.Validate)
|
||||
|
||||
// Configuration updates require admin permissions
|
||||
cfgWrite := cfg.Group("/")
|
||||
if casbinSvc != nil {
|
||||
cfgWrite.Use(middleware.RequireCasbinPermission(casbinSvc, "/v1/config", "POST"))
|
||||
} else {
|
||||
cfgWrite.Use(middleware.RequireRole("admin"))
|
||||
}
|
||||
{
|
||||
cfgWrite.POST("", configH.Update)
|
||||
}
|
||||
}
|
||||
|
||||
// Chaos Engineering
|
||||
chaosGroup := apiProtected.Group("/chaos")
|
||||
{
|
||||
chaosGroup.GET("/experiments", chaosH.List)
|
||||
chaosGroup.GET("/status", chaosH.Status)
|
||||
|
||||
// Running chaos experiments requires admin permissions
|
||||
chaosWrite := chaosGroup.Group("/")
|
||||
if casbinSvc != nil {
|
||||
chaosWrite.Use(middleware.RequireCasbinPermission(casbinSvc, "/v1/chaos", "POST"))
|
||||
} else {
|
||||
chaosWrite.Use(middleware.RequireRole("admin"))
|
||||
}
|
||||
{
|
||||
chaosWrite.POST("/experiments", chaosH.Run)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create server
|
||||
server := &http.Server{
|
||||
Addr: ":" + cfg.Server.Port,
|
||||
@@ -120,6 +403,9 @@ func main() {
|
||||
// Start periodic metrics collection
|
||||
go startMetricsCollection(ctx, metricsCollector, db)
|
||||
|
||||
// Start alert manager
|
||||
go monitoring.StartAlertManager(ctx)
|
||||
|
||||
// Start server in a goroutine
|
||||
go func() {
|
||||
log.Printf("Starting API server on port %s", cfg.Server.Port)
|
||||
|
||||
@@ -1,231 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
var (
|
||||
logger zerolog.Logger
|
||||
dbClient *mongo.Client
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Initialize logger
|
||||
logger = zerolog.New(os.Stdout).With().
|
||||
Timestamp().
|
||||
Str("service", "api-server").
|
||||
Logger()
|
||||
|
||||
// Load environment variables
|
||||
if err := godotenv.Load(); err != nil {
|
||||
logger.Info().Msg("No .env file found, using system environment")
|
||||
}
|
||||
|
||||
// Connect to MongoDB
|
||||
mongoURI := getEnv("MONGODB_URI", "mongodb://localhost:27017/free5gc")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var err error
|
||||
dbClient, err = mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
|
||||
if err != nil {
|
||||
logger.Fatal().Err(err).Msg("Failed to connect to MongoDB")
|
||||
}
|
||||
defer dbClient.Disconnect(context.Background())
|
||||
|
||||
// Verify connection
|
||||
if err := dbClient.Ping(ctx, nil); err != nil {
|
||||
logger.Fatal().Err(err).Msg("Failed to ping MongoDB")
|
||||
}
|
||||
logger.Info().Msg("Connected to MongoDB successfully")
|
||||
|
||||
// Initialize Gin router
|
||||
router := setupRouter()
|
||||
|
||||
// Start server
|
||||
port := getEnv("API_PORT", "8000")
|
||||
logger.Info().Str("port", port).Msg("Starting API server")
|
||||
if err := router.Run(":" + port); err != nil {
|
||||
logger.Fatal().Err(err).Msg("Failed to start server")
|
||||
}
|
||||
}
|
||||
|
||||
func setupRouter() *gin.Engine {
|
||||
// Set Gin mode
|
||||
if getEnv("GIN_MODE", "debug") == "release" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
router := gin.Default()
|
||||
|
||||
// Health check
|
||||
router.GET("/health", healthCheck)
|
||||
|
||||
// API v1 routes
|
||||
v1 := router.Group("/v1")
|
||||
{
|
||||
// eSIM management
|
||||
v1.POST("/esims", createESIM)
|
||||
v1.GET("/esims/:id", getESIM)
|
||||
v1.GET("/esims/:id/usage", getESIMUsage)
|
||||
v1.DELETE("/esims/:id", deleteESIM)
|
||||
|
||||
// Subscriber management
|
||||
v1.GET("/subscribers", listSubscribers)
|
||||
v1.GET("/subscribers/:imsi", getSubscriber)
|
||||
}
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
// Health check endpoint
|
||||
func healthCheck(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": "healthy",
|
||||
"service": "api-server",
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// POST /v1/esims - Create new eSIM
|
||||
func createESIM(c *gin.Context) {
|
||||
var req struct {
|
||||
DataPlan string `json:"data_plan" binding:"required"`
|
||||
CountryCode string `json:"country_code" binding:"required"`
|
||||
Carrier string `json:"carrier,omitempty"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate IMSI (simplified - use proper allocation in production)
|
||||
imsi := fmt.Sprintf("208930%09d", time.Now().UnixNano()%1000000000)
|
||||
|
||||
// Using Go 1.26's new(expr) for optional pointer fields
|
||||
esim := gin.H{
|
||||
"esim_id": uuid.New().String(),
|
||||
"imsi": imsi,
|
||||
"iccid": generateICCID(imsi),
|
||||
"activation_code": fmt.Sprintf("LPA:1$smdp.example.com$%s", uuid.New().String()),
|
||||
"status": "provisioned",
|
||||
"data_plan": req.DataPlan,
|
||||
"country_code": req.CountryCode,
|
||||
"created_at": time.Now(),
|
||||
}
|
||||
|
||||
logger.Info().
|
||||
Str("esim_id", esim["esim_id"].(string)).
|
||||
Str("imsi", imsi).
|
||||
Msg("eSIM created")
|
||||
|
||||
c.JSON(http.StatusCreated, esim)
|
||||
}
|
||||
|
||||
// GET /v1/esims/:id - Get eSIM details
|
||||
func getESIM(c *gin.Context) {
|
||||
esimID := c.Param("id")
|
||||
|
||||
// TODO: Implement actual database lookup
|
||||
esim := gin.H{
|
||||
"esim_id": esimID,
|
||||
"imsi": "208930000000001",
|
||||
"iccid": "8933123456789012345",
|
||||
"status": "active",
|
||||
"data_plan": "1GB",
|
||||
"created_at": time.Now().Add(-24 * time.Hour),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, esim)
|
||||
}
|
||||
|
||||
// GET /v1/esims/:id/usage - Get eSIM usage statistics
|
||||
func getESIMUsage(c *gin.Context) {
|
||||
esimID := c.Param("id")
|
||||
|
||||
usage := gin.H{
|
||||
"esim_id": esimID,
|
||||
"data_used": 250 * 1024 * 1024, // 250 MB in bytes
|
||||
"data_limit": 1024 * 1024 * 1024, // 1 GB in bytes
|
||||
"sessions": []gin.H{
|
||||
{
|
||||
"started_at": time.Now().Add(-2 * time.Hour),
|
||||
"ended_at": time.Now().Add(-1 * time.Hour),
|
||||
"bytes_up": 10 * 1024 * 1024,
|
||||
"bytes_down": 50 * 1024 * 1024,
|
||||
},
|
||||
},
|
||||
"updated_at": time.Now(),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, usage)
|
||||
}
|
||||
|
||||
// DELETE /v1/esims/:id - Terminate eSIM
|
||||
func deleteESIM(c *gin.Context) {
|
||||
esimID := c.Param("id")
|
||||
|
||||
logger.Info().Str("esim_id", esimID).Msg("eSIM terminated")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "eSIM terminated successfully",
|
||||
"esim_id": esimID,
|
||||
})
|
||||
}
|
||||
|
||||
// GET /v1/subscribers - List all subscribers
|
||||
func listSubscribers(c *gin.Context) {
|
||||
// TODO: Implement MongoDB query
|
||||
subscribers := []gin.H{
|
||||
{
|
||||
"imsi": "208930000000001",
|
||||
"status": "active",
|
||||
"created_at": time.Now().Add(-7 * 24 * time.Hour),
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"subscribers": subscribers,
|
||||
"count": len(subscribers),
|
||||
})
|
||||
}
|
||||
|
||||
// GET /v1/subscribers/:imsi - Get subscriber details
|
||||
func getSubscriber(c *gin.Context) {
|
||||
imsi := c.Param("imsi")
|
||||
|
||||
subscriber := gin.H{
|
||||
"imsi": imsi,
|
||||
"plmn_id": "20893",
|
||||
"status": "active",
|
||||
"created_at": time.Now().Add(-7 * 24 * time.Hour),
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, subscriber)
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
func getEnv(key, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func generateICCID(imsi string) string {
|
||||
// ICCID format: 89 (Telecom) + CC (Country) + ISSUER + ACCOUNT + CHECK
|
||||
// Simplified implementation
|
||||
return fmt.Sprintf("893312%s%d", imsi[5:14], time.Now().Unix()%10)
|
||||
}
|
||||
Reference in New Issue
Block a user