feat: Refactor carrier-connector main.go to extract route setup and handlers into separate packages for improved modularity

- Move setupRoutes to routes.go with ProfileRepository parameter and handler package integration
- Extract all handler functions (orderProfile/getProfile/listProfiles/deleteProfile/getCarrierInfo/checkConnectivity) to internal/handler package
- Move ProfileOrder/ProfileResponse structs to internal/handler package as shared types
- Move getEnv helper function to handler.Get
This commit is contained in:
nutcas3
2026-04-24 08:50:41 +03:00
parent d27709222e
commit 24586bbeb4
2 changed files with 60 additions and 327 deletions
+19 -327
View File
@@ -1,360 +1,52 @@
package main
import (
"context"
"fmt"
"net/http"
"os"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/config"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/es2"
handler "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/handler"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
"github.com/rs/zerolog"
)
var logger zerolog.Logger
// GSMA ES2+ Profile Order Request
type ProfileOrder struct {
EID string `json:"eid"`
ICCID string `json:"iccid"`
IMSI string `json:"imsi"`
K string `json:"k"`
OPc string `json:"opc"`
MCC string `json:"mcc"`
MNC string `json:"mnc"`
ProfileType string `json:"profileType"`
ConfirmationCode string `json:"confirmationCode,omitempty"`
}
// GSMA ES2+ Profile Order Response
type ProfileResponse struct {
ExecutionStatus string `json:"executionStatus"`
StatusMessage string `json:"statusMessage"`
ProfileID string `json:"profileId"`
ActivationCode string `json:"activationCode,omitempty"`
}
func main() {
// Initialize logger
logger = zerolog.New(os.Stdout).With().
handler.Logger = zerolog.New(os.Stdout).With().
Timestamp().
Str("service", "carrier-connector").
Logger()
// Load environment variables
if err := godotenv.Load(); err != nil {
logger.Info().Msg("No .env file found, using system environment")
handler.Logger.Info().Msg("No .env file found, using system environment")
}
// Initialize ES2+ client with GSMA protocol
smdpURL := getEnv("SMDP_URL", "https://smdp.example.com")
apiKey := getEnv("SMDP_API_KEY", "test-api-key")
requesterID := getEnv("FUNCTIONALITY_REQUESTER_ID", "carrier-connector")
insecure := getEnv("INSECURE_SKIP_VERIFY", "false") == "true"
port := getEnv("PORT", "8080")
client := es2.NewES2Client(&config.ES2Config{
BaseURL: handler.GetEnv("SMDP_URL", "https://smdp.example.com"),
APIKey: handler.GetEnv("SMDP_API_KEY", "test-api-key"),
FunctionalityRequesterID: handler.GetEnv("FUNCTIONALITY_REQUESTER_ID", "carrier-connector"),
InsecureSkipVerify: handler.GetEnv("INSECURE_SKIP_VERIFY", "true") == "true",
})
port := handler.GetEnv("PORT", "8080")
es2Config := &config.ES2Config{
BaseURL: smdpURL,
APIKey: apiKey,
FunctionalityRequesterID: requesterID,
InsecureSkipVerify: insecure,
}
client := es2.NewES2Client(es2Config)
// Initialize Gin router
router := gin.Default()
// Add middleware for logging
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
router.Use(gin.LoggerWithFormatter(func(p 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,
p.ClientIP, p.TimeStamp.Format(time.RFC1123),
p.Method, p.Path, p.Request.Proto,
p.StatusCode, p.Latency, p.Request.UserAgent(), p.ErrorMessage,
)
}))
// Add recovery middleware
router.Use(gin.Recovery())
// Initialize API routes
setupRoutes(router, client)
profileRepo := repository.NewInMemoryProfileStore()
setupRoutes(router, client, profileRepo)
logger.Info().
Str("port", port).
Msg("Carrier Connector API server starting")
// Start server
handler.Logger.Info().Str("port", port).Msg("Carrier Connector API server starting")
if err := router.Run(":" + port); err != nil {
logger.Fatal().Err(err).Msg("Failed to start server")
handler.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))
}
// Carrier management endpoints
carrier := api.Group("/carrier")
{
// Get carrier info
carrier.GET("/info", getCarrierInfoHandler(client))
// Check SM-DP+ connectivity
carrier.GET("/connectivity", checkConnectivityHandler(client))
}
}
// API Handlers
func orderProfileHandler(client *es2.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+")
// Convert ProfileOrder to DownloadProfileRequest for GSMA ES2+ protocol
downloadReq := &es2.DownloadProfileRequest{
EID: order.EID,
ICCID: order.ICCID,
ProfileType: order.ProfileType,
ConfirmationCode: order.ConfirmationCode,
}
downloadResp, err := client.DownloadProfile(context.Background(), downloadReq)
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("execution_status", downloadResp.ExecutionStatus).
Str("status_message", downloadResp.StatusMessage).
Str("imsi", order.IMSI).
Msg("Profile ordered successfully via API")
// Convert GSMA response to our API response format
response := &ProfileResponse{
ExecutionStatus: downloadResp.ExecutionStatus,
StatusMessage: downloadResp.StatusMessage,
ProfileID: order.ICCID, // Use ICCID as profile ID for our API
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"profile": response,
})
}
}
func getProfileHandler(client *es2.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 *es2.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 *es2.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 *es2.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": "https://smdp.example.com", // Use config value
"supported": true,
"lastChecked": time.Now().UTC(),
},
})
}
}
func checkConnectivityHandler(client *es2.ES2Client) gin.HandlerFunc {
return func(c *gin.Context) {
logger.Info().Msg("API request: Checking SM-DP+ connectivity")
// Simple connectivity check using the ES2+ client
// Use a GetProfileStatus request with dummy data to test connectivity
req := &es2.GetProfileStatusRequest{
EID: "test-eid",
ICCID: "test-iccid",
}
_, err := client.GetProfileStatus(context.Background(), req)
isConnected := err == nil
statusCode := 200
if err != nil {
statusCode = 500 // Assume connection error
}
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
}
return fallback
}
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/es2"
handler "github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/handler"
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/repository"
)
// setupRoutes registers all HTTP routes, wiring the ES2+ client and profile repo.
func setupRoutes(router *gin.Engine, client *es2.ES2Client, repo repository.ProfileRepository) {
api := router.Group("/api/v1")
api.GET("/health", healthHandler)
esim := api.Group("/esim")
{
esim.POST("/profiles", handler.OrderProfileHandlerWithRepo(client, repo))
esim.GET("/profiles", handler.ListProfilesHandlerWithRepo(repo))
esim.GET("/profiles/:profileId", handler.GetProfileHandlerWithRepo(client, repo))
esim.DELETE("/profiles/:profileId", handler.DeleteProfileHandlerWithRepo(client, repo))
}
carrier := api.Group("/carrier")
{
carrier.GET("/info", handler.GetCarrierInfoHandler(client))
carrier.GET("/connectivity", handler.CheckConnectivityHandler(client))
}
}
// healthHandler returns a simple liveness response.
func healthHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"service": "carrier-connector",
"timestamp": time.Now().UTC(),
})
}