2025-12-03 19:29:37 +00:00
|
|
|
package handlers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
"github.com/google/uuid"
|
|
|
|
|
"go.uber.org/zap"
|
2025-12-03 19:29:37 +00:00
|
|
|
"veza-backend-api/internal/services"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// AnalyticsHandler gère les opérations d'analytics de lecture de tracks
|
|
|
|
|
type AnalyticsHandler struct {
|
|
|
|
|
analyticsService *services.AnalyticsService
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
commonHandler *CommonHandler
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewAnalyticsHandler crée un nouveau handler d'analytics
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
func NewAnalyticsHandler(analyticsService *services.AnalyticsService, logger *zap.Logger) *AnalyticsHandler {
|
|
|
|
|
return &AnalyticsHandler{
|
|
|
|
|
analyticsService: analyticsService,
|
|
|
|
|
commonHandler: NewCommonHandler(logger),
|
|
|
|
|
}
|
2025-12-03 19:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecordPlayRequest représente la requête pour enregistrer une lecture
|
|
|
|
|
type RecordPlayRequest struct {
|
|
|
|
|
Duration int `json:"duration" binding:"required,min=1"`
|
|
|
|
|
Device string `json:"device,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RecordPlay gère l'enregistrement d'une lecture de track
|
|
|
|
|
func (h *AnalyticsHandler) RecordPlay(c *gin.Context) {
|
|
|
|
|
trackIDStr := c.Param("id")
|
|
|
|
|
if trackIDStr == "" {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var req RecordPlayRequest
|
P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.
Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.
Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).
Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.
Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 10:14:38 +00:00
|
|
|
if appErr := h.commonHandler.BindAndValidateJSON(c, &req); appErr != nil {
|
|
|
|
|
RespondWithAppError(c, appErr)
|
2025-12-03 19:29:37 +00:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer user_id si authentifié (optionnel pour analytics anonymes)
|
|
|
|
|
var userID *uuid.UUID
|
|
|
|
|
if uid, ok := c.Get("user_id"); ok {
|
|
|
|
|
if uidUUID, ok := uid.(uuid.UUID); ok {
|
|
|
|
|
userID = &uidUUID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Récupérer IP address et device
|
|
|
|
|
ipAddress := c.ClientIP()
|
|
|
|
|
device := req.Device
|
|
|
|
|
if device == "" {
|
|
|
|
|
device = c.GetHeader("User-Agent")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
err = h.analyticsService.RecordPlay(c.Request.Context(), trackID, userID, req.Duration, device, ipAddress)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err.Error() == "track not found" {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"message": "play recorded"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetTrackStats gère la récupération des statistiques d'un track
|
|
|
|
|
func (h *AnalyticsHandler) GetTrackStats(c *gin.Context) {
|
|
|
|
|
trackIDStr := c.Param("id")
|
|
|
|
|
if trackIDStr == "" {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stats, err := h.analyticsService.GetTrackStats(c.Request.Context(), trackID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err.Error() == "track not found" {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"stats": stats})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetTopTracks gère la récupération des tracks les plus écoutés
|
|
|
|
|
func (h *AnalyticsHandler) GetTopTracks(c *gin.Context) {
|
|
|
|
|
// Parse limit
|
|
|
|
|
limit := 10
|
|
|
|
|
if limitStr := c.Query("limit"); limitStr != "" {
|
|
|
|
|
if l, err := strconv.Atoi(limitStr); err == nil && l > 0 && l <= 100 {
|
|
|
|
|
limit = l
|
|
|
|
|
} else {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid limit (must be between 1 and 100)"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse start_date (optionnel)
|
|
|
|
|
var startDate *time.Time
|
|
|
|
|
if startDateStr := c.Query("start_date"); startDateStr != "" {
|
|
|
|
|
parsed, err := time.Parse(time.RFC3339, startDateStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid start_date format (use RFC3339)"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
startDate = &parsed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse end_date (optionnel)
|
|
|
|
|
var endDate *time.Time
|
|
|
|
|
if endDateStr := c.Query("end_date"); endDateStr != "" {
|
|
|
|
|
parsed, err := time.Parse(time.RFC3339, endDateStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid end_date format (use RFC3339)"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
endDate = &parsed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
topTracks, err := h.analyticsService.GetTopTracks(c.Request.Context(), limit, startDate, endDate)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"tracks": topTracks})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetPlaysOverTime gère la récupération des lectures sur une période
|
|
|
|
|
func (h *AnalyticsHandler) GetPlaysOverTime(c *gin.Context) {
|
|
|
|
|
trackIDStr := c.Param("id")
|
|
|
|
|
if trackIDStr == "" {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "track id is required"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
trackID, err := uuid.Parse(trackIDStr) // Changed to uuid.Parse
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid track id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse start_date (optionnel, défaut: 30 jours)
|
|
|
|
|
startDate := time.Now().AddDate(0, 0, -30)
|
|
|
|
|
if startDateStr := c.Query("start_date"); startDateStr != "" {
|
|
|
|
|
parsed, err := time.Parse(time.RFC3339, startDateStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid start_date format (use RFC3339)"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
startDate = parsed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse end_date (optionnel, défaut: maintenant)
|
|
|
|
|
endDate := time.Now()
|
|
|
|
|
if endDateStr := c.Query("end_date"); endDateStr != "" {
|
|
|
|
|
parsed, err := time.Parse(time.RFC3339, endDateStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid end_date format (use RFC3339)"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
endDate = parsed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Parse interval (optionnel, défaut: day)
|
|
|
|
|
interval := c.DefaultQuery("interval", "day")
|
|
|
|
|
validIntervals := map[string]bool{"hour": true, "day": true, "week": true, "month": true}
|
|
|
|
|
if !validIntervals[interval] {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid interval (must be: hour, day, week, month)"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
points, err := h.analyticsService.GetPlaysOverTime(c.Request.Context(), trackID, startDate, endDate, interval)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err.Error() == "track not found" {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "track not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"points": points})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetUserStats gère la récupération des statistiques d'un utilisateur
|
|
|
|
|
func (h *AnalyticsHandler) GetUserStats(c *gin.Context) {
|
|
|
|
|
userIDStr := c.Param("id")
|
|
|
|
|
if userIDStr == "" {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "user id is required"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
userID, err := uuid.Parse(userIDStr)
|
|
|
|
|
if err != nil {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid user id"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Vérifier que l'utilisateur peut accéder à ses propres stats
|
|
|
|
|
var authenticatedUserID *uuid.UUID
|
|
|
|
|
if uid, ok := c.Get("user_id"); ok {
|
|
|
|
|
if uidUUID, ok := uid.(uuid.UUID); ok {
|
|
|
|
|
authenticatedUserID = &uidUUID
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if authenticatedUserID != nil && *authenticatedUserID != userID {
|
|
|
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "cannot access other user's stats"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stats, err := h.analyticsService.GetUserStats(c.Request.Context(), userID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err.Error() == "user not found" {
|
|
|
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, gin.H{"stats": stats})
|
|
|
|
|
}
|