mirror of
https://github.com/andwati/telecom-platform.git
synced 2026-10-18 11:02:54 +00:00
- Add bluemonday import to auth.go and subscriber.go - Add UGCPolicy sanitization for username, email, firstName, lastName, and role in Register - Add conditional XSS sanitization for firstName, lastName, email, and MSISDN in CreateSubscriber and UpdateSubscriber - Replace Page and PageSize with Cursor and Limit in ListSubscribers query parameters - Add HATEOAS links to CreateSubscriber, GetSubscriber
351 lines
9.9 KiB
Go
351 lines
9.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/microcosm-cc/bluemonday"
|
|
"github.com/nutcas3/telecom-platform/apps/api-server/internal/models"
|
|
"github.com/nutcas3/telecom-platform/apps/api-server/internal/services"
|
|
)
|
|
|
|
// SubscriberHandler handles subscriber-related HTTP requests
|
|
type SubscriberHandler struct {
|
|
subscriberService *services.SubscriberService
|
|
}
|
|
|
|
// NewSubscriberHandler creates a new subscriber handler
|
|
func NewSubscriberHandler(subscriberService *services.SubscriberService) *SubscriberHandler {
|
|
return &SubscriberHandler{
|
|
subscriberService: subscriberService,
|
|
}
|
|
}
|
|
|
|
// CreateSubscriber creates a new subscriber
|
|
// @Summary Create a new subscriber
|
|
// @Description Create a new subscriber with allocated IMSI
|
|
// @Tags subscribers
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param subscriber body services.CreateSubscriberRequest true "Subscriber data"
|
|
// @Success 201 {object} models.Subscriber
|
|
// @Failure 400 {object} ErrorResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /api/v1/subscribers [post]
|
|
func (h *SubscriberHandler) CreateSubscriber(c *gin.Context) {
|
|
var req services.CreateSubscriberRequest
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Error: "Invalid request body",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Sanitize user input to prevent XSS
|
|
policy := bluemonday.UGCPolicy()
|
|
if req.FirstName != "" {
|
|
req.FirstName = policy.Sanitize(req.FirstName)
|
|
}
|
|
if req.LastName != "" {
|
|
req.LastName = policy.Sanitize(req.LastName)
|
|
}
|
|
if req.Email != "" {
|
|
req.Email = policy.Sanitize(req.Email)
|
|
}
|
|
if req.MSISDN != "" {
|
|
req.MSISDN = policy.Sanitize(req.MSISDN)
|
|
}
|
|
|
|
subscriber, err := h.subscriberService.CreateSubscriber(c.Request.Context(), &req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Error: "Failed to create subscriber",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Add HATEOAS links
|
|
baseURL := c.Request.Host
|
|
if c.Request.TLS != nil {
|
|
baseURL = "https://" + baseURL
|
|
} else {
|
|
baseURL = "http://" + baseURL
|
|
}
|
|
|
|
subscriberWithLinks := models.SubscriberWithLinks{
|
|
Subscriber: *subscriber,
|
|
Links: models.NewSubscriberLinks(baseURL, subscriber),
|
|
}
|
|
|
|
c.JSON(http.StatusCreated, subscriberWithLinks)
|
|
}
|
|
|
|
// GetSubscriber retrieves a subscriber by ID
|
|
// @Summary Get subscriber by ID
|
|
// @Description Retrieve subscriber information by ID
|
|
// @Tags subscribers
|
|
// @Produce json
|
|
// @Param id path int true "Subscriber ID"
|
|
// @Success 200 {object} models.Subscriber
|
|
// @Failure 404 {object} ErrorResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /api/v1/subscribers/{id} [get]
|
|
func (h *SubscriberHandler) GetSubscriber(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Error: "Invalid subscriber ID",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
subscriber, err := h.subscriberService.GetSubscriber(c.Request.Context(), uint(id))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, ErrorResponse{
|
|
Error: "Subscriber not found",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Add HATEOAS links
|
|
baseURL := c.Request.Host
|
|
if c.Request.TLS != nil {
|
|
baseURL = "https://" + baseURL
|
|
} else {
|
|
baseURL = "http://" + baseURL
|
|
}
|
|
|
|
subscriberWithLinks := models.SubscriberWithLinks{
|
|
Subscriber: *subscriber,
|
|
Links: models.NewSubscriberLinks(baseURL, subscriber),
|
|
}
|
|
|
|
c.JSON(http.StatusOK, subscriberWithLinks)
|
|
}
|
|
|
|
// GetSubscriberByIMSI retrieves a subscriber by IMSI
|
|
// @Summary Get subscriber by IMSI
|
|
// @Description Retrieve subscriber information by IMSI
|
|
// @Tags subscribers
|
|
// @Produce json
|
|
// @Param imsi path string true "IMSI"
|
|
// @Success 200 {object} models.Subscriber
|
|
// @Failure 404 {object} ErrorResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /api/v1/subscribers/imsi/{imsi} [get]
|
|
func (h *SubscriberHandler) GetSubscriberByIMSI(c *gin.Context) {
|
|
imsi := models.IMSI(c.Param("imsi"))
|
|
|
|
subscriber, err := h.subscriberService.GetSubscriberByIMSI(c.Request.Context(), imsi)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, ErrorResponse{
|
|
Error: "Subscriber not found",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, subscriber)
|
|
}
|
|
|
|
// UpdateSubscriber updates subscriber information
|
|
// @Summary Update subscriber
|
|
// @Description Update subscriber information
|
|
// @Tags subscribers
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param id path int true "Subscriber ID"
|
|
// @Param subscriber body services.UpdateSubscriberRequest true "Updated subscriber data"
|
|
// @Success 200 {object} models.Subscriber
|
|
// @Failure 400 {object} ErrorResponse
|
|
// @Failure 404 {object} ErrorResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /api/v1/subscribers/{id} [put]
|
|
func (h *SubscriberHandler) UpdateSubscriber(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Error: "Invalid subscriber ID",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
var req services.UpdateSubscriberRequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Error: "Invalid request body",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Sanitize user input to prevent XSS
|
|
policy := bluemonday.UGCPolicy()
|
|
if req.FirstName != nil && *req.FirstName != "" {
|
|
sanitized := policy.Sanitize(*req.FirstName)
|
|
req.FirstName = &sanitized
|
|
}
|
|
if req.LastName != nil && *req.LastName != "" {
|
|
sanitized := policy.Sanitize(*req.LastName)
|
|
req.LastName = &sanitized
|
|
}
|
|
if req.Email != nil && *req.Email != "" {
|
|
sanitized := policy.Sanitize(*req.Email)
|
|
req.Email = &sanitized
|
|
}
|
|
|
|
subscriber, err := h.subscriberService.UpdateSubscriber(c.Request.Context(), uint(id), &req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Error: "Failed to update subscriber",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, subscriber)
|
|
}
|
|
|
|
// ListSubscribers lists subscribers with pagination and filtering
|
|
// @Summary List subscribers
|
|
// @Description List subscribers with pagination and filtering
|
|
// @Tags subscribers
|
|
// @Produce json
|
|
// @Param page query int false "Page number" default(1)
|
|
// @Param page_size query int false "Page size" default(20)
|
|
// @Param status query string false "Subscriber status"
|
|
// @Param organization_id query string false "Organization ID"
|
|
// @Param search query string false "Search term"
|
|
// @Success 200 {object} services.ListSubscribersResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /api/v1/subscribers [get]
|
|
func (h *SubscriberHandler) ListSubscribers(c *gin.Context) {
|
|
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
|
|
|
if limit > 100 {
|
|
limit = 100
|
|
}
|
|
|
|
req := &services.ListSubscribersRequest{
|
|
Cursor: c.Query("cursor"),
|
|
Limit: limit,
|
|
Status: models.SubscriberStatus(c.Query("status")),
|
|
OrganizationID: c.Query("organization_id"),
|
|
Search: c.Query("search"),
|
|
}
|
|
|
|
response, err := h.subscriberService.ListSubscribers(c.Request.Context(), req)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Error: "Failed to list subscribers",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
// Add HATEOAS links
|
|
baseURL := c.Request.Host
|
|
if c.Request.TLS != nil {
|
|
baseURL = "https://" + baseURL
|
|
} else {
|
|
baseURL = "http://" + baseURL
|
|
}
|
|
|
|
subscribersWithLinks := make([]models.SubscriberWithLinks, len(response.Subscribers))
|
|
for i, subscriber := range response.Subscribers {
|
|
subscribersWithLinks[i] = models.SubscriberWithLinks{
|
|
Subscriber: subscriber,
|
|
Links: models.NewSubscriberLinks(baseURL, &subscriber),
|
|
}
|
|
}
|
|
|
|
listResponse := models.SubscriberListWithLinks{
|
|
Subscribers: subscribersWithLinks,
|
|
NextCursor: response.NextCursor,
|
|
HasMore: response.HasMore,
|
|
Links: models.NewSubscriberListLinks(baseURL, response.NextCursor, req.Limit, response.HasMore),
|
|
}
|
|
|
|
c.JSON(http.StatusOK, listResponse)
|
|
}
|
|
|
|
// SuspendSubscriber suspends a subscriber
|
|
// @Summary Suspend subscriber
|
|
// @Description Suspend a subscriber and terminate their sessions
|
|
// @Tags subscribers
|
|
// @Produce json
|
|
// @Param id path int true "Subscriber ID"
|
|
// @Success 204
|
|
// @Failure 404 {object} ErrorResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /api/v1/subscribers/{id}/suspend [post]
|
|
func (h *SubscriberHandler) SuspendSubscriber(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Error: "Invalid subscriber ID",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
if err := h.subscriberService.SuspendSubscriber(c.Request.Context(), uint(id)); err != nil {
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Error: "Failed to suspend subscriber",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// TerminateSubscriber terminates a subscriber
|
|
// @Summary Terminate subscriber
|
|
// @Description Terminate a subscriber and deactivate their eSIM profile
|
|
// @Tags subscribers
|
|
// @Produce json
|
|
// @Param id path int true "Subscriber ID"
|
|
// @Success 204
|
|
// @Failure 404 {object} ErrorResponse
|
|
// @Failure 500 {object} ErrorResponse
|
|
// @Router /api/v1/subscribers/{id}/terminate [post]
|
|
func (h *SubscriberHandler) TerminateSubscriber(c *gin.Context) {
|
|
idStr := c.Param("id")
|
|
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, ErrorResponse{
|
|
Error: "Invalid subscriber ID",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
if err := h.subscriberService.TerminateSubscriber(c.Request.Context(), uint(id)); err != nil {
|
|
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
|
Error: "Failed to terminate subscriber",
|
|
Details: err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
|
|
// ErrorResponse represents an API error response
|
|
type SubscriberErrorResponse struct {
|
|
Error string `json:"error"`
|
|
Code string `json:"code"`
|
|
Details string `json:"details,omitempty"`
|
|
}
|