From 0e28b8a1ac0411df4beb1df890d834418f43ba59 Mon Sep 17 00:00:00 2001 From: nutcas3 Date: Mon, 20 Apr 2026 18:09:53 +0300 Subject: [PATCH] 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 --- apps/api-server/cmd/server.go | 286 ++++++++++++++++++++++++++++++++++ apps/api-server/main.go | 231 --------------------------- 2 files changed, 286 insertions(+), 231 deletions(-) delete mode 100644 apps/api-server/main.go diff --git a/apps/api-server/cmd/server.go b/apps/api-server/cmd/server.go index 177de8c..1ce67ea 100644 --- a/apps/api-server/cmd/server.go +++ b/apps/api-server/cmd/server.go @@ -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) diff --git a/apps/api-server/main.go b/apps/api-server/main.go deleted file mode 100644 index 6a00fb7..0000000 --- a/apps/api-server/main.go +++ /dev/null @@ -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) -}