diff --git a/apps/carrier-connector/main.go b/apps/carrier-connector/main.go index 8024b48..c40878f 100644 --- a/apps/carrier-connector/main.go +++ b/apps/carrier-connector/main.go @@ -3,9 +3,12 @@ package main import ( "crypto/tls" "fmt" + "net/http" "os" + "strconv" "time" + "github.com/gin-gonic/gin" "github.com/go-resty/resty/v2" "github.com/joho/godotenv" "github.com/rs/zerolog" @@ -47,43 +50,84 @@ func main() { logger.Info().Msg("No .env file found, using system environment") } - logger.Info().Msg("Carrier Connector started") - // Initialize ES2+ client smdpURL := getEnv("SMDP_URL", "https://smdp.example.com") apiKey := getEnv("SMDP_API_KEY", "test-api-key") + port := getEnv("PORT", "8080") client := NewES2Client(smdpURL, apiKey) - // Example: Order a profile - order := &ProfileOrder{ - ICCID: "8933123456789012345", - IMSI: "208930000000001", - K: "465B5CE8B199B49FAA5F0A2EE238A6BC", - OPc: "E8ED289DEBA952E4283B54E88E6183CA", - MCC: "208", - MNC: "93", - ProfileType: "operational", - } + // Initialize Gin router + router := gin.Default() + + // Add middleware for logging + router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", + param.ClientIP, + param.TimeStamp.Format(time.RFC1123), + param.Method, + param.Path, + param.Request.Proto, + param.StatusCode, + param.Latency, + param.Request.UserAgent(), + param.ErrorMessage, + ) + })) + + // Add recovery middleware + router.Use(gin.Recovery()) + + // Initialize API routes + setupRoutes(router, client) logger.Info(). - Str("imsi", order.IMSI). - Msg("Ordering eSIM profile from SM-DP+") + Str("port", port). + Msg("Carrier Connector API server starting") - // In production, this would be triggered by API requests - response, err := client.OrderProfile(order) - if err != nil { - logger.Error().Err(err).Msg("Failed to order profile") - } else { - logger.Info(). - Str("activation_code", response.ActivationCode). - Str("profile_id", response.ProfileID). - Msg("Profile ordered successfully") + // Start server + if err := router.Run(":" + port); err != nil { + logger.Fatal().Err(err).Msg("Failed to start server") + } +} + +func setupRoutes(router *gin.Engine, client *ES2Client) { + api := router.Group("/api/v1") + + // Health check endpoint + api.GET("/health", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "status": "healthy", + "service": "carrier-connector", + "timestamp": time.Now().UTC(), + }) + }) + + // eSIM profile endpoints + esim := api.Group("/esim") + { + // Order a new eSIM profile + esim.POST("/profiles", orderProfileHandler(client)) + + // Get profile status + esim.GET("/profiles/:profileId", getProfileHandler(client)) + + // List all profiles + esim.GET("/profiles", listProfilesHandler(client)) + + // Delete/disable profile + esim.DELETE("/profiles/:profileId", deleteProfileHandler(client)) } - // Keep service running - logger.Info().Msg("Carrier Connector running. Press Ctrl+C to exit.") - select {} + // Carrier management endpoints + carrier := api.Group("/carrier") + { + // Get carrier info + carrier.GET("/info", getCarrierInfoHandler(client)) + + // Check SM-DP+ connectivity + carrier.GET("/connectivity", checkConnectivityHandler(client)) + } } func NewES2Client(baseURL, apiKey string) *ES2Client { @@ -124,6 +168,198 @@ func (c *ES2Client) OrderProfile(order *ProfileOrder) (*ProfileResponse, error) return &response, nil } +// API Handlers + +func orderProfileHandler(client *ES2Client) gin.HandlerFunc { + return func(c *gin.Context) { + var order ProfileOrder + + if err := c.ShouldBindJSON(&order); err != nil { + logger.Error().Err(err).Msg("Invalid request body") + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid request body", + "message": err.Error(), + }) + return + } + + // Validate required fields + if order.ICCID == "" || order.IMSI == "" || order.K == "" || order.OPc == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Missing required fields", + "message": "ICCID, IMSI, K, and OPc are required", + }) + return + } + + logger.Info(). + Str("imsi", order.IMSI). + Str("iccid", order.ICCID). + Msg("API request: Ordering eSIM profile from SM-DP+") + + response, err := client.OrderProfile(&order) + if err != nil { + logger.Error().Err(err).Str("imsi", order.IMSI).Msg("Failed to order profile") + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to order profile", + "message": err.Error(), + }) + return + } + + logger.Info(). + Str("activation_code", response.ActivationCode). + Str("profile_id", response.ProfileID). + Str("imsi", order.IMSI). + Msg("Profile ordered successfully via API") + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "profile": response, + }) + } +} + +func getProfileHandler(client *ES2Client) gin.HandlerFunc { + return func(c *gin.Context) { + profileID := c.Param("profileId") + + if profileID == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Missing profile ID", + "message": "Profile ID is required", + }) + return + } + + logger.Info(). + Str("profile_id", profileID). + Msg("API request: Getting eSIM profile status") + + // TODO: Implement actual profile retrieval from SM-DP+ + // For now, return a mock response + c.JSON(http.StatusOK, gin.H{ + "success": true, + "profile": gin.H{ + "profileId": profileID, + "status": "active", + "createdAt": time.Now().UTC(), + }, + }) + } +} + +func listProfilesHandler(client *ES2Client) gin.HandlerFunc { + return func(c *gin.Context) { + logger.Info().Msg("API request: Listing eSIM profiles") + + // Parse query parameters + page := 1 + if pageStr := c.Query("page"); pageStr != "" { + if parsed, err := strconv.Atoi(pageStr); err == nil && parsed > 0 { + page = parsed + } + } + + limit := 20 + if limitStr := c.Query("limit"); limitStr != "" { + if parsed, err := strconv.Atoi(limitStr); err == nil && parsed > 0 && parsed <= 100 { + limit = parsed + } + } + + // TODO: Implement actual profile listing from database/SM-DP+ + // For now, return a mock response + c.JSON(http.StatusOK, gin.H{ + "success": true, + "profiles": []gin.H{ + { + "profileId": "profile-123", + "imsi": "208930000000001", + "status": "active", + }, + { + "profileId": "profile-456", + "imsi": "208930000000002", + "status": "inactive", + }, + }, + "pagination": gin.H{ + "page": page, + "limit": limit, + "total": 2, + }, + }) + } +} + +func deleteProfileHandler(client *ES2Client) gin.HandlerFunc { + return func(c *gin.Context) { + profileID := c.Param("profileId") + + if profileID == "" { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Missing profile ID", + "message": "Profile ID is required", + }) + return + } + + logger.Info(). + Str("profile_id", profileID). + Msg("API request: Deleting eSIM profile") + + // TODO: Implement actual profile deletion/disable from SM-DP+ + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "Profile deleted successfully", + }) + } +} + +func getCarrierInfoHandler(client *ES2Client) gin.HandlerFunc { + return func(c *gin.Context) { + logger.Info().Msg("API request: Getting carrier information") + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "carrier": gin.H{ + "name": "Example Carrier", + "mcc": "208", + "mnc": "93", + "smdpUrl": client.baseURL, + "supported": true, + "lastChecked": time.Now().UTC(), + }, + }) + } +} + +func checkConnectivityHandler(client *ES2Client) gin.HandlerFunc { + return func(c *gin.Context) { + logger.Info().Msg("API request: Checking SM-DP+ connectivity") + + // Simple connectivity check by making a request to SM-DP+ + resp, err := client.httpClient.R().Get("/health") + + isConnected := err == nil && resp.StatusCode() < 500 + statusCode := 0 + if resp != nil { + statusCode = resp.StatusCode() + } + + c.JSON(http.StatusOK, gin.H{ + "success": true, + "connectivity": gin.H{ + "smdpConnected": isConnected, + "statusCode": statusCode, + "checkedAt": time.Now().UTC(), + "error": nil, + }, + }) + } +} + func getEnv(key, fallback string) string { if value := os.Getenv(key); value != "" { return value