veza/veza-backend-api/internal/services/track_share_service.go
2025-12-16 11:23:49 -05:00

179 lines
4.8 KiB
Go

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
}