package services import ( "bytes" "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "net/http" "time" "veza-backend-api/internal/models" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" ) // WebhookService gère les webhooks type WebhookService struct { db *gorm.DB logger *zap.Logger secret string client *http.Client } // WebhookPayload représente le payload d'un webhook type WebhookPayload struct { Event string `json:"event"` Timestamp time.Time `json:"timestamp"` Data map[string]interface{} `json:"data"` } // NewWebhookService crée un nouveau service de webhooks func NewWebhookService(db *gorm.DB, logger *zap.Logger, secret string) *WebhookService { return &WebhookService{ db: db, logger: logger, secret: secret, client: &http.Client{ Timeout: 10 * time.Second, }, } } // RegisterWebhook enregistre une nouvelle URL de webhook func (s *WebhookService) RegisterWebhook(ctx context.Context, userID uuid.UUID, url string, events []string) (*models.Webhook, error) { webhook := &models.Webhook{ UserID: userID, URL: url, Events: events, Active: true, CreatedAt: time.Now(), } if err := s.db.WithContext(ctx).Create(webhook).Error; err != nil { return nil, fmt.Errorf("failed to register webhook: %w", err) } s.logger.Info("Webhook registered", zap.String("user_id", userID.String()), zap.String("url", url), zap.Strings("events", events)) return webhook, nil } // DeliverWebhook envoie un webhook avec retry et signature HMAC func (s *WebhookService) DeliverWebhook(ctx context.Context, webhook *models.Webhook, event string, data map[string]interface{}) error { payload := WebhookPayload{ Event: event, Timestamp: time.Now(), Data: data, } jsonData, err := json.Marshal(payload) if err != nil { return fmt.Errorf("failed to marshal payload: %w", err) } // Générer signature HMAC signature := s.generateSignature(jsonData) // Créer la requête HTTP req, err := http.NewRequestWithContext(ctx, "POST", webhook.URL, bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Veza-Signature", signature) req.Header.Set("X-Veza-Event", event) req.Header.Set("X-Veza-Timestamp", payload.Timestamp.Format(time.RFC3339)) // Envoyer avec retry maxRetries := 3 backoff := time.Second for i := 0; i < maxRetries; i++ { resp, err := s.client.Do(req) if err != nil { s.logger.Warn("Webhook delivery failed, retrying", zap.Int("attempt", i+1), zap.Error(err)) if i < maxRetries-1 { time.Sleep(backoff) backoff *= 2 // Exponential backoff continue } return fmt.Errorf("webhook delivery failed after %d attempts: %w", maxRetries, err) } defer resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 300 { s.logger.Info("Webhook delivered successfully", zap.String("url", webhook.URL), zap.String("event", event)) return nil } s.logger.Warn("Webhook returned non-200 status", zap.String("url", webhook.URL), zap.Int("status", resp.StatusCode)) } return fmt.Errorf("webhook delivery failed") } // generateSignature génère une signature HMAC-SHA256 func (s *WebhookService) generateSignature(payload []byte) string { mac := hmac.New(sha256.New, []byte(s.secret)) mac.Write(payload) return hex.EncodeToString(mac.Sum(nil)) } // VerifySignature vérifie une signature HMAC func (s *WebhookService) VerifySignature(signature string, payload []byte) bool { expected := s.generateSignature(payload) return hmac.Equal([]byte(signature), []byte(expected)) } // TriggerEvent déclenche un événement pour tous les webhooks concernés func (s *WebhookService) TriggerEvent(ctx context.Context, event string, data map[string]interface{}, userID *uuid.UUID) error { // Récupérer les webhooks actifs pour cet événement var webhooks []models.Webhook query := s.db.WithContext(ctx).Where("active = ? AND events @> ARRAY[?]", true, event) if userID != nil { query = query.Where("user_id = ?", *userID) } if err := query.Find(&webhooks).Error; err != nil { return fmt.Errorf("failed to fetch webhooks: %w", err) } // Envoyer les webhooks en async for _, webhook := range webhooks { go func(w models.Webhook) { if err := s.DeliverWebhook(ctx, &w, event, data); err != nil { s.logger.Error("Failed to deliver webhook", zap.Error(err), zap.String("url", w.URL), zap.String("event", event)) } }(webhook) } return nil } // ListWebhooks liste les webhooks d'un utilisateur func (s *WebhookService) ListWebhooks(ctx context.Context, userID uuid.UUID) ([]models.Webhook, error) { var webhooks []models.Webhook if err := s.db.WithContext(ctx). Where("user_id = ?", userID). Find(&webhooks).Error; err != nil { return nil, fmt.Errorf("failed to list webhooks: %w", err) } return webhooks, nil } // GetWebhook récupère un webhook par son ID et userID func (s *WebhookService) GetWebhook(ctx context.Context, webhookID, userID uuid.UUID) (*models.Webhook, error) { var webhook models.Webhook if err := s.db.WithContext(ctx). Where("id = ? AND user_id = ?", webhookID, userID). First(&webhook).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("webhook not found") } return nil, fmt.Errorf("failed to get webhook: %w", err) } return &webhook, nil } // DeleteWebhook supprime un webhook func (s *WebhookService) DeleteWebhook(ctx context.Context, webhookID, userID uuid.UUID) error { result := s.db.WithContext(ctx). Where("id = ? AND user_id = ?", webhookID, userID). Delete(&models.Webhook{}) if result.Error != nil { return fmt.Errorf("failed to delete webhook: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("webhook not found") } return nil }