package services import ( "context" "crypto/rand" "encoding/hex" "errors" "time" "github.com/google/uuid" "veza-backend-api/internal/models" "gorm.io/gorm" ) 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 }