package services import ( "context" "crypto/rand" "encoding/hex" "errors" "fmt" "strings" "time" "veza-backend-api/internal/models" "github.com/google/uuid" "gorm.io/gorm" ) var ( // ErrShareNotFound est retourné quand un share n'est pas trouvé ErrShareNotFound = errors.New("share not found") // ErrShareExpired est retourné quand un share a expiré ErrShareExpired = errors.New("share link expired") // ErrSharePermissionDenied est retourné quand la permission demandée n'est pas accordée ErrSharePermissionDenied = errors.New("permission denied") ) // TrackShareService gère le partage de tracks type TrackShareService struct { db *gorm.DB } // NewTrackShareService crée un nouveau service de partage de tracks func NewTrackShareService(db *gorm.DB) *TrackShareService { return &TrackShareService{db: db} } // generateShareToken génère un token unique sécurisé func generateShareToken() (string, error) { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", err } return hex.EncodeToString(bytes), nil } // CreateShare crée un nouveau lien de partage pour un track // MIGRATION UUID: Completée. UserID et TrackID en UUID. func (s *TrackShareService) CreateShare(ctx context.Context, trackID uuid.UUID, userID uuid.UUID, permissions string, expiresAt *time.Time) (*models.TrackShare, error) { // Vérifier que le track existe et appartient à l'utilisateur var track models.Track if err := s.db.First(&track, "id = ?", trackID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrTrackNotFound } return nil, err } if track.UserID != userID { return nil, ErrForbidden } // Générer un token unique token, err := generateShareToken() if err != nil { return nil, err } // Vérifier l'unicité du token (très peu probable mais on vérifie) var existingShare models.TrackShare for { if err := s.db.Where("share_token = ?", token).First(&existingShare).Error; errors.Is(err, gorm.ErrRecordNotFound) { break } token, err = generateShareToken() if err != nil { return nil, err } } share := &models.TrackShare{ TrackID: trackID, UserID: userID, ShareToken: token, Permissions: permissions, ExpiresAt: expiresAt, AccessCount: 0, } if err := s.db.Create(share).Error; err != nil { return nil, err } return share, nil } // ValidateShareToken valide un token de partage et retourne le share func (s *TrackShareService) ValidateShareToken(ctx context.Context, token string) (*models.TrackShare, error) { var share models.TrackShare if err := s.db.Where("share_token = ? AND deleted_at IS NULL", token).First(&share).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrShareNotFound } return nil, err } // Vérifier l'expiration if share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now()) { return nil, ErrShareExpired } // Incrémenter le compteur d'accès s.db.Model(&share).Update("access_count", gorm.Expr("access_count + 1")) // Recharger l'objet pour obtenir la valeur mise à jour if err := s.db.First(&share, share.ID).Error; err != nil { return nil, fmt.Errorf("failed to reload share: %w", err) } return &share, nil } // CheckPermission vérifie si un share a une permission spécifique func (s *TrackShareService) CheckPermission(share *models.TrackShare, permission string) bool { if share == nil { return false } // Vérifier l'expiration if share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now()) { return false } // Vérifier les permissions permissions := strings.Split(share.Permissions, ",") for _, p := range permissions { if strings.TrimSpace(strings.ToLower(p)) == strings.ToLower(permission) { return true } } return false } // GetShareByToken récupère un share par son token (sans incrémenter le compteur) func (s *TrackShareService) GetShareByToken(ctx context.Context, token string) (*models.TrackShare, error) { var share models.TrackShare if err := s.db.Where("share_token = ? AND deleted_at IS NULL", token).First(&share).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrShareNotFound } return nil, err } // Vérifier l'expiration if share.ExpiresAt != nil && share.ExpiresAt.Before(time.Now()) { return nil, ErrShareExpired } return &share, nil } // RevokeShare révoque un lien de partage // MIGRATION UUID: Completée. UserID et ShareID en UUID. func (s *TrackShareService) RevokeShare(ctx context.Context, shareID uuid.UUID, userID uuid.UUID) error { var share models.TrackShare if err := s.db.First(&share, "id = ?", shareID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return ErrShareNotFound } return err } // Vérifier que l'utilisateur est le propriétaire if share.UserID != userID { return ErrForbidden } // Soft delete return s.db.Delete(&share).Error }