333 lines
11 KiB
Go
333 lines
11 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
|
|
apperrors "veza-backend-api/internal/errors"
|
|
"veza-backend-api/internal/services"
|
|
"veza-backend-api/internal/workers"
|
|
)
|
|
|
|
// WebhookHandler gère les handlers de webhooks
|
|
type WebhookHandler struct {
|
|
webhookService *services.WebhookService
|
|
webhookWorker *workers.WebhookWorker
|
|
logger *zap.Logger
|
|
commonHandler *CommonHandler
|
|
}
|
|
|
|
// NewWebhookHandler crée un nouveau handler de webhooks
|
|
func NewWebhookHandler(
|
|
webhookService *services.WebhookService,
|
|
webhookWorker *workers.WebhookWorker,
|
|
logger *zap.Logger,
|
|
) *WebhookHandler {
|
|
return &WebhookHandler{
|
|
webhookService: webhookService,
|
|
webhookWorker: webhookWorker,
|
|
logger: logger,
|
|
commonHandler: NewCommonHandler(logger),
|
|
}
|
|
}
|
|
|
|
// RegisterWebhook gère l'enregistrement d'un webhook
|
|
// @Summary Register webhook
|
|
// @Description Register a new webhook for receiving events
|
|
// @Tags Webhook
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param webhook body object true "Webhook registration data" SchemaExample({"url": "https://example.com/webhook", "events": ["track.uploaded", "playlist.created"]})
|
|
// @Success 201 {object} handlers.APIResponse{data=object{webhook=object}}
|
|
// @Failure 400 {object} handlers.APIResponse "Validation error"
|
|
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
|
|
// @Failure 500 {object} handlers.APIResponse "Internal server error"
|
|
// @Router /webhooks [post]
|
|
func (h *WebhookHandler) RegisterWebhook() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// Récupérer l'ID utilisateur
|
|
userIDInterface, exists := c.Get("user_id")
|
|
if !exists {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInvalidCredentials, "User not authenticated"))
|
|
return
|
|
}
|
|
|
|
userID, ok := userIDInterface.(uuid.UUID)
|
|
if !ok {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "Invalid user ID type"))
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
URL string `json:"url" binding:"required,url"`
|
|
Events []string `json:"events" binding:"required,min=1"`
|
|
}
|
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
RespondWithAppError(c, appErr)
|
|
return
|
|
}
|
|
|
|
webhook, err := h.webhookService.RegisterWebhook(c.Request.Context(), userID, req.URL, req.Events)
|
|
if err != nil {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to register webhook", err))
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusCreated, webhook)
|
|
}
|
|
}
|
|
|
|
// ListWebhooks liste les webhooks d'un utilisateur
|
|
// @Summary List webhooks
|
|
// @Description Get a list of all webhooks registered by the current user
|
|
// @Tags Webhook
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Success 200 {object} handlers.APIResponse{data=object{webhooks=array}}
|
|
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
|
|
// @Failure 500 {object} handlers.APIResponse "Internal server error"
|
|
// @Router /webhooks [get]
|
|
func (h *WebhookHandler) ListWebhooks() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userIDInterface, exists := c.Get("user_id")
|
|
if !exists {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInvalidCredentials, "User not authenticated"))
|
|
return
|
|
}
|
|
|
|
userID, ok := userIDInterface.(uuid.UUID)
|
|
if !ok {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "Invalid user ID type"))
|
|
return
|
|
}
|
|
|
|
webhooks, err := h.webhookService.ListWebhooks(c.Request.Context(), userID)
|
|
if err != nil {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to list webhooks", err))
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, webhooks)
|
|
}
|
|
}
|
|
|
|
// DeleteWebhook supprime un webhook
|
|
// @Summary Delete webhook
|
|
// @Description Delete a webhook by ID
|
|
// @Tags Webhook
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path string true "Webhook ID"
|
|
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
|
|
// @Failure 400 {object} handlers.APIResponse "Invalid webhook ID"
|
|
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
|
|
// @Failure 404 {object} handlers.APIResponse "Webhook not found"
|
|
// @Router /webhooks/{id} [delete]
|
|
func (h *WebhookHandler) DeleteWebhook() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userIDInterface, exists := c.Get("user_id")
|
|
if !exists {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInvalidCredentials, "User not authenticated"))
|
|
return
|
|
}
|
|
|
|
userID, ok := userIDInterface.(uuid.UUID)
|
|
if !ok {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "Invalid user ID type"))
|
|
return
|
|
}
|
|
|
|
webhookIDStr := c.Param("id")
|
|
webhookID, err := uuid.Parse(webhookIDStr)
|
|
if err != nil {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "Invalid webhook ID"))
|
|
return
|
|
}
|
|
|
|
err = h.webhookService.DeleteWebhook(c.Request.Context(), webhookID, userID)
|
|
if err != nil {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeNotFound, "Webhook not found"))
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": "Webhook deleted successfully"})
|
|
}
|
|
}
|
|
|
|
// GetWebhookStats retourne les statistiques des webhooks
|
|
// @Summary Get webhook statistics
|
|
// @Description Get statistics for webhook delivery and performance
|
|
// @Tags Webhook
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Success 200 {object} handlers.APIResponse{data=object{stats=object}}
|
|
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
|
|
// @Failure 500 {object} handlers.APIResponse "Internal server error"
|
|
// @Router /webhooks/stats [get]
|
|
// BE-API-033: Implement webhook stats endpoint validation
|
|
func (h *WebhookHandler) GetWebhookStats() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// Récupérer l'ID utilisateur depuis le contexte (pour cohérence avec les autres endpoints protégés)
|
|
userID, ok := GetUserIDUUID(c)
|
|
if !ok {
|
|
return // Erreur déjà envoyée par GetUserIDUUID
|
|
}
|
|
|
|
// Récupérer les statistiques du worker
|
|
if h.webhookWorker == nil {
|
|
h.logger.Error("WebhookWorker not available")
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Webhook stats service not available", nil))
|
|
return
|
|
}
|
|
|
|
stats := h.webhookWorker.GetStats()
|
|
|
|
// BE-API-033: Standardize response format
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"user_id": userID,
|
|
"stats": stats,
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWebhook teste un webhook
|
|
// @Summary Test webhook
|
|
// @Description Send a test event to a webhook to verify it's working
|
|
// @Tags Webhook
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path string true "Webhook ID"
|
|
// @Success 200 {object} handlers.APIResponse{data=object{message=string}}
|
|
// @Failure 400 {object} handlers.APIResponse "Invalid webhook ID"
|
|
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
|
|
// @Failure 404 {object} handlers.APIResponse "Webhook not found"
|
|
// @Router /webhooks/{id}/test [post]
|
|
func (h *WebhookHandler) TestWebhook() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userIDInterface, exists := c.Get("user_id")
|
|
if !exists {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInvalidCredentials, "User not authenticated"))
|
|
return
|
|
}
|
|
|
|
userID, ok := userIDInterface.(uuid.UUID)
|
|
if !ok {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "Invalid user ID type"))
|
|
return
|
|
}
|
|
|
|
webhookIDStr := c.Param("id")
|
|
webhookID, err := uuid.Parse(webhookIDStr)
|
|
if err != nil {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "Invalid webhook ID"))
|
|
return
|
|
}
|
|
|
|
webhook, err := h.webhookService.GetWebhook(c.Request.Context(), webhookID, userID)
|
|
if err != nil {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeNotFound, "Webhook not found"))
|
|
return
|
|
}
|
|
|
|
// INT-008: Standardize date format to ISO 8601 (RFC3339)
|
|
job := workers.WebhookJob{
|
|
Webhook: webhook,
|
|
Event: "ping",
|
|
Data: map[string]interface{}{
|
|
"message": "This is a test webhook from Veza",
|
|
"timestamp": time.Now().UTC().Format(time.RFC3339),
|
|
"test_id": uuid.New().String(),
|
|
},
|
|
Retries: 0,
|
|
}
|
|
|
|
h.webhookWorker.Enqueue(job)
|
|
|
|
h.logger.Info("Test webhook queued", zap.String("webhook_id", webhookID.String()))
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{"message": fmt.Sprintf("Webhook test queued for %s", webhookID)})
|
|
}
|
|
}
|
|
|
|
// RegenerateAPIKey régénère la clé API d'un webhook
|
|
// @Summary Regenerate webhook API key
|
|
// @Description Generate a new API key for a webhook (invalidates the old one)
|
|
// @Tags Webhook
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Security BearerAuth
|
|
// @Param id path string true "Webhook ID"
|
|
// @Success 200 {object} handlers.APIResponse{data=object{api_key=string,message=string}}
|
|
// @Failure 400 {object} handlers.APIResponse "Invalid webhook ID"
|
|
// @Failure 401 {object} handlers.APIResponse "Unauthorized"
|
|
// @Failure 404 {object} handlers.APIResponse "Webhook not found"
|
|
// @Failure 500 {object} handlers.APIResponse "Internal server error"
|
|
// @Router /webhooks/{id}/regenerate-key [post]
|
|
// BE-SEC-012: Regenerate webhook API key
|
|
func (h *WebhookHandler) RegenerateAPIKey() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userIDInterface, exists := c.Get("user_id")
|
|
if !exists {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInvalidCredentials, "User not authenticated"))
|
|
return
|
|
}
|
|
|
|
userID, ok := userIDInterface.(uuid.UUID)
|
|
if !ok {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeInternal, "Invalid user ID type"))
|
|
return
|
|
}
|
|
|
|
webhookIDStr := c.Param("id")
|
|
webhookID, err := uuid.Parse(webhookIDStr)
|
|
if err != nil {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeValidation, "Invalid webhook ID"))
|
|
return
|
|
}
|
|
|
|
newAPIKey, err := h.webhookService.RegenerateAPIKey(c.Request.Context(), webhookID, userID)
|
|
if err != nil {
|
|
if err.Error() == "webhook not found" {
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.New(apperrors.ErrCodeNotFound, "Webhook not found"))
|
|
return
|
|
}
|
|
// INT-006: Standardize error response format
|
|
RespondWithAppError(c, apperrors.Wrap(apperrors.ErrCodeInternal, "Failed to regenerate API key", err))
|
|
return
|
|
}
|
|
|
|
RespondSuccess(c, http.StatusOK, gin.H{
|
|
"api_key": newAPIKey,
|
|
"message": "API key regenerated successfully",
|
|
})
|
|
}
|
|
}
|