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" validate:"required,url"` Events []string `json:"events" validate:"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", }) } }