veza/veza-backend-api/internal/handlers/webhook_handlers.go
senke 3cfefaa24c [BE-SEC-012] be-sec: Implement API key authentication for webhooks
- Added APIKey field to Webhook model with unique index
- Implemented GenerateAPIKey() method using crypto/rand for secure key generation
- Implemented ValidateAPIKey() method to authenticate webhook requests
- Implemented RegenerateAPIKey() method to rotate API keys
- Created WebhookAPIKeyMiddleware for validating API keys in requests
- Middleware supports X-API-Key header and Authorization: Bearer format
- Added endpoint POST /api/v1/webhooks/:id/regenerate-key
- API keys are prefixed with 'whk_' for identification
- Comprehensive unit tests for all API key functionality
- Inactive webhooks cannot authenticate with their API keys

Phase: PHASE-4
Priority: P2
Progress: 119/267 (44.57%)
2025-12-24 18:03:52 +01:00

244 lines
6.6 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
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 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "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 {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to register webhook"})
return
}
RespondSuccess(c, http.StatusCreated, webhook)
}
}
// ListWebhooks liste les webhooks d'un utilisateur
func (h *WebhookHandler) ListWebhooks() gin.HandlerFunc {
return func(c *gin.Context) {
userIDInterface, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
return
}
webhooks, err := h.webhookService.ListWebhooks(c.Request.Context(), userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to list webhooks"})
return
}
RespondSuccess(c, http.StatusOK, webhooks)
}
}
// DeleteWebhook supprime un webhook
func (h *WebhookHandler) DeleteWebhook() gin.HandlerFunc {
return func(c *gin.Context) {
userIDInterface, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
return
}
webhookIDStr := c.Param("id")
webhookID, err := uuid.Parse(webhookIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid webhook ID"})
return
}
err = h.webhookService.DeleteWebhook(c.Request.Context(), webhookID, userID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Webhook not found"})
return
}
RespondSuccess(c, http.StatusOK, gin.H{"message": "Webhook deleted successfully"})
}
}
// GetWebhookStats retourne les statistiques des webhooks
// GET /api/v1/webhooks/stats
// 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
func (h *WebhookHandler) TestWebhook() gin.HandlerFunc {
return func(c *gin.Context) {
userIDInterface, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
return
}
webhookIDStr := c.Param("id")
webhookID, err := uuid.Parse(webhookIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid webhook ID"})
return
}
webhook, err := h.webhookService.GetWebhook(c.Request.Context(), webhookID, userID)
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Webhook not found"})
return
}
job := workers.WebhookJob{
Webhook: webhook,
Event: "ping",
Data: map[string]interface{}{
"message": "This is a test webhook from Veza",
"timestamp": time.Now(),
"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 (BE-SEC-012)
func (h *WebhookHandler) RegenerateAPIKey() gin.HandlerFunc {
return func(c *gin.Context) {
userIDInterface, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
return
}
userID, ok := userIDInterface.(uuid.UUID)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type"})
return
}
webhookIDStr := c.Param("id")
webhookID, err := uuid.Parse(webhookIDStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid webhook ID"})
return
}
newAPIKey, err := h.webhookService.RegenerateAPIKey(c.Request.Context(), webhookID, userID)
if err != nil {
if err.Error() == "webhook not found" {
c.JSON(http.StatusNotFound, gin.H{"error": "Webhook not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to regenerate API key"})
return
}
RespondSuccess(c, http.StatusOK, gin.H{
"api_key": newAPIKey,
"message": "API key regenerated successfully",
})
}
}