mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-31 00:52:16 +00:00
- Add context import and config/es2 package imports for GSMA ES2+ protocol implementation - Remove ES2Client struct definition and move to internal/es2 package - Update ProfileOrder struct with EID/ConfirmationCode fields for GSMA ES2+ compliance - Update ProfileResponse struct with ExecutionStatus/StatusMessage fields replacing Status - Add ES2Config initialization with Base
388 lines
9.5 KiB
Go
388 lines
9.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"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/nutcas3/telecom-platform/apps/carrier-connector/internal/config"
|
|
"github.com/nutcas3/telecom-platform/apps/carrier-connector/internal/es2"
|
|
"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().
|
|
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")
|
|
}
|
|
|
|
// 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")
|
|
|
|
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 {
|
|
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("port", port).
|
|
Msg("Carrier Connector API server starting")
|
|
|
|
// 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))
|
|
}
|
|
|
|
// 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 {
|
|
client := resty.New().
|
|
SetBaseURL(baseURL).
|
|
SetHeader("Authorization", fmt.Sprintf("Bearer %s", apiKey)).
|
|
SetHeader("Content-Type", "application/json").
|
|
SetTimeout(10 * time.Second).
|
|
SetTLSClientConfig(&tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
})
|
|
|
|
return &ES2Client{
|
|
baseURL: baseURL,
|
|
apiKey: apiKey,
|
|
httpClient: client,
|
|
}
|
|
}
|
|
|
|
func (c *ES2Client) OrderProfile(order *ProfileOrder) (*ProfileResponse, error) {
|
|
var response ProfileResponse
|
|
|
|
// NOTE: This is a simplified example
|
|
// In production, implement full GSMA ES2+ protocol
|
|
resp, err := c.httpClient.R().
|
|
SetBody(order).
|
|
SetResult(&response).
|
|
Post("/gsma/rsp2/es2plus/createProfile")
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ES2+ API call failed: %w", err)
|
|
}
|
|
|
|
if resp.StatusCode() != 200 {
|
|
return nil, fmt.Errorf("SM-DP+ returned error: %s", resp.String())
|
|
}
|
|
|
|
return &response, nil
|
|
}
|
|
|
|
// 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("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
|
|
}
|
|
return fallback
|
|
}
|