veza/veza-backend-api/internal/services/playlist_share_service.go

191 lines
6.2 KiB
Go

package services
import (
"context"
"crypto/rand"
"encoding/hex"
"errors"
"github.com/google/uuid"
"time"
"gorm.io/gorm"
"veza-backend-api/internal/models"
)
var (
// ErrPlaylistShareNotFound est retourné quand un share de playlist n'est pas trouvé
ErrPlaylistShareNotFound = errors.New("playlist share not found")
// ErrPlaylistShareExpired est retourné quand un share de playlist a expiré
ErrPlaylistShareExpired = errors.New("playlist share link expired")
)
// PlaylistShareService gère le partage de playlists
// T0488: Create Playlist Public Share Link
type PlaylistShareService struct {
db *gorm.DB
}
// NewPlaylistShareService crée un nouveau service de partage de playlists
func NewPlaylistShareService(db *gorm.DB) *PlaylistShareService {
return &PlaylistShareService{db: db}
}
// generateShareToken génère un token unique sécurisé
func generatePlaylistShareToken() (string, error) {
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}
// CreateShareLink crée un nouveau lien de partage public pour une playlist
// T0488: Create Playlist Public Share Link
// MIGRATION UUID: Completée. playlistID et userID sont des UUIDs.
func (s *PlaylistShareService) CreateShareLink(ctx context.Context, playlistID uuid.UUID, userID uuid.UUID, expiresAt *time.Time) (*models.PlaylistShareLink, error) {
// Vérifier que la playlist existe et appartient à l'utilisateur
var playlist models.Playlist
if err := s.db.First(&playlist, "id = ?", playlistID).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("playlist not found")
}
return nil, err
}
// Vérifier que l'utilisateur est le propriétaire ou a la permission admin
if playlist.UserID != userID {
// Vérifier si l'utilisateur est collaborateur avec permission admin
var collaborator models.PlaylistCollaborator
if err := s.db.Where("playlist_id = ? AND user_id = ? AND permission = ?", playlistID, userID, models.PlaylistPermissionAdmin).First(&collaborator).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errors.New("forbidden: only owner or admin can create share links")
}
return nil, err
}
}
// Vérifier si un lien de partage existe déjà pour cette playlist
var existingLink models.PlaylistShareLink
if err := s.db.Where("playlist_id = ? AND deleted_at IS NULL", playlistID).First(&existingLink).Error; err == nil {
// Un lien existe déjà, vérifier s'il est expiré
if existingLink.ExpiresAt != nil && existingLink.ExpiresAt.Before(time.Now()) {
// Le lien est expiré, on le supprime (soft delete) et on en crée un nouveau
s.db.Delete(&existingLink)
} else {
// Le lien existe et est valide, on le retourne
return &existingLink, nil
}
}
// Générer un token unique
token, err := generatePlaylistShareToken()
if err != nil {
return nil, err
}
// Vérifier l'unicité du token (très peu probable mais on vérifie)
var existingShare models.PlaylistShareLink
for {
if err := s.db.Where("share_token = ?", token).First(&existingShare).Error; errors.Is(err, gorm.ErrRecordNotFound) {
break
}
token, err = generatePlaylistShareToken()
if err != nil {
return nil, err
}
}
shareLink := &models.PlaylistShareLink{
PlaylistID: playlistID,
UserID: userID,
ShareToken: token,
ExpiresAt: expiresAt,
AccessCount: 0,
}
if err := s.db.Create(shareLink).Error; err != nil {
return nil, err
}
return shareLink, nil
}
// ValidateShareToken valide un token de partage et retourne le share link
// T0488: Create Playlist Public Share Link
func (s *PlaylistShareService) ValidateShareToken(ctx context.Context, token string) (*models.PlaylistShareLink, error) {
var shareLink models.PlaylistShareLink
if err := s.db.Where("share_token = ? AND deleted_at IS NULL", token).First(&shareLink).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrPlaylistShareNotFound
}
return nil, err
}
// Vérifier l'expiration
if shareLink.ExpiresAt != nil && shareLink.ExpiresAt.Before(time.Now()) {
return nil, ErrPlaylistShareExpired
}
// Incrémenter le compteur d'accès
s.db.Model(&shareLink).Update("access_count", gorm.Expr("access_count + 1"))
return &shareLink, nil
}
// GetShareLinkByToken récupère un share link par son token (sans incrémenter le compteur)
// T0488: Create Playlist Public Share Link
func (s *PlaylistShareService) GetShareLinkByToken(ctx context.Context, token string) (*models.PlaylistShareLink, error) {
var shareLink models.PlaylistShareLink
if err := s.db.Where("share_token = ? AND deleted_at IS NULL", token).First(&shareLink).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrPlaylistShareNotFound
}
return nil, err
}
// Vérifier l'expiration
if shareLink.ExpiresAt != nil && shareLink.ExpiresAt.Before(time.Now()) {
return nil, ErrPlaylistShareExpired
}
return &shareLink, nil
}
// RevokeShareLink révoque un lien de partage
// T0488: Create Playlist Public Share Link
func (s *PlaylistShareService) RevokeShareLink(ctx context.Context, shareLinkID, userID uuid.UUID) error {
var shareLink models.PlaylistShareLink
if err := s.db.First(&shareLink, "id = ?", shareLinkID).Error; err != nil { // UUID query
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrPlaylistShareNotFound
}
return err
}
// Vérifier que l'utilisateur est le propriétaire
if shareLink.UserID != userID {
return errors.New("forbidden")
}
// Soft delete
return s.db.Delete(&shareLink).Error
}
// GetShareLinkByPlaylistID récupère le lien de partage actif pour une playlist
// T0488: Create Playlist Public Share Link
func (s *PlaylistShareService) GetShareLinkByPlaylistID(ctx context.Context, playlistID uuid.UUID) (*models.PlaylistShareLink, error) {
var shareLink models.PlaylistShareLink
if err := s.db.Where("playlist_id = ? AND deleted_at IS NULL", playlistID).First(&shareLink).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrPlaylistShareNotFound
}
return nil, err
}
// Vérifier l'expiration
if shareLink.ExpiresAt != nil && shareLink.ExpiresAt.Before(time.Now()) {
return nil, ErrPlaylistShareExpired
}
return &shareLink, nil
}