veza/veza-backend-api/internal/services/data_export_service.go
senke b8adaf8935 [BE-SVC-022] be-svc: Implement data export service
- Created DataExportService for comprehensive user data export (GDPR compliance)
- Exports all user data: profile, settings, tracks, playlists, comments, likes, analytics, federated identities, roles
- Added ExportUserData method to retrieve all user data from database
- Added ExportUserDataAsJSON method to export as downloadable JSON file
- Added endpoint GET /api/v1/users/me/export that returns JSON file download
- Comprehensive unit tests for export service
- Proper error handling and logging

Phase: PHASE-6
Priority: P2
Progress: 118/267 (44.19%)
2025-12-24 18:01:00 +01:00

362 lines
12 KiB
Go

package services
import (
"context"
"encoding/json"
"fmt"
"time"
"veza-backend-api/internal/models"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
)
// DataExportService gère l'export de données utilisateur pour la conformité GDPR (BE-SVC-022)
type DataExportService struct {
db *gorm.DB
logger *zap.Logger
}
// NewDataExportService crée un nouveau service d'export de données
func NewDataExportService(db *gorm.DB, logger *zap.Logger) *DataExportService {
return &DataExportService{
db: db,
logger: logger,
}
}
// UserDataExport représente toutes les données d'un utilisateur à exporter
type UserDataExport struct {
UserID uuid.UUID `json:"user_id"`
ExportedAt time.Time `json:"exported_at"`
Profile *UserProfileExport `json:"profile"`
Settings *UserSettingsExport `json:"settings"`
Tracks []TrackExport `json:"tracks"`
Playlists []PlaylistExport `json:"playlists"`
Comments []CommentExport `json:"comments"`
Likes []LikeExport `json:"likes"`
Analytics []AnalyticsExport `json:"analytics"`
FederatedIDs []FederatedIDExport `json:"federated_identities"`
Roles []RoleExport `json:"roles"`
}
// UserProfileExport représente les données de profil utilisateur
type UserProfileExport struct {
ID uuid.UUID `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
Location string `json:"location"`
Birthdate *time.Time `json:"birthdate"`
Gender string `json:"gender"`
Role string `json:"role"`
IsActive bool `json:"is_active"`
IsVerified bool `json:"is_verified"`
IsPublic bool `json:"is_public"`
LastLoginAt *time.Time `json:"last_login_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// UserSettingsExport représente les paramètres utilisateur
type UserSettingsExport struct {
EmailNotifications bool `json:"email_notifications"`
PushNotifications bool `json:"push_notifications"`
BrowserNotifications bool `json:"browser_notifications"`
EmailOnFollow bool `json:"email_on_follow"`
EmailOnLike bool `json:"email_on_like"`
EmailOnComment bool `json:"email_on_comment"`
EmailOnMessage bool `json:"email_on_message"`
EmailOnMention bool `json:"email_on_mention"`
EmailMarketing bool `json:"email_marketing"`
AllowSearchIndexing bool `json:"allow_search_indexing"`
ShowActivity bool `json:"show_activity"`
ExplicitContent bool `json:"explicit_content"`
Autoplay bool `json:"autoplay"`
}
// TrackExport représente un track exporté
type TrackExport struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Artist string `json:"artist"`
Album string `json:"album"`
Duration int `json:"duration"`
Genre string `json:"genre"`
Year int `json:"year"`
FilePath string `json:"file_path"`
CoverArtPath string `json:"cover_art_path"`
IsPublic bool `json:"is_public"`
Status string `json:"status"`
PlayCount int64 `json:"play_count"`
LikeCount int64 `json:"like_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// PlaylistExport représente une playlist exportée
type PlaylistExport struct {
ID uuid.UUID `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
IsPublic bool `json:"is_public"`
TrackCount int `json:"track_count"`
FollowerCount int `json:"follower_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CommentExport représente un commentaire exporté
type CommentExport struct {
ID uuid.UUID `json:"id"`
TrackID uuid.UUID `json:"track_id"`
Content string `json:"content"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// LikeExport représente un like exporté
type LikeExport struct {
ID uuid.UUID `json:"id"`
TrackID uuid.UUID `json:"track_id"`
CreatedAt time.Time `json:"created_at"`
}
// AnalyticsExport représente des analytics exportées
type AnalyticsExport struct {
ID uuid.UUID `json:"id"`
TrackID uuid.UUID `json:"track_id"`
PlayTime int `json:"play_time"`
PauseCount int `json:"pause_count"`
SeekCount int `json:"seek_count"`
CompletionRate float64 `json:"completion_rate"`
StartedAt time.Time `json:"started_at"`
EndedAt *time.Time `json:"ended_at"`
CreatedAt time.Time `json:"created_at"`
}
// FederatedIDExport représente une identité fédérée exportée
type FederatedIDExport struct {
ID uuid.UUID `json:"id"`
Provider string `json:"provider"`
ProviderID string `json:"provider_id"`
Email string `json:"email"`
DisplayName string `json:"display_name"`
CreatedAt time.Time `json:"created_at"`
}
// RoleExport représente un rôle exporté
type RoleExport struct {
ID uuid.UUID `json:"id"`
RoleName string `json:"role_name"`
AssignedAt time.Time `json:"assigned_at"`
ExpiresAt *time.Time `json:"expires_at"`
IsActive bool `json:"is_active"`
}
// ExportUserData exporte toutes les données d'un utilisateur (BE-SVC-022)
func (s *DataExportService) ExportUserData(ctx context.Context, userID uuid.UUID) (*UserDataExport, error) {
export := &UserDataExport{
UserID: userID,
ExportedAt: time.Now(),
}
// 1. Récupérer le profil utilisateur
var user models.User
if err := s.db.WithContext(ctx).First(&user, "id = ?", userID).Error; err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}
export.Profile = &UserProfileExport{
ID: user.ID,
Username: user.Username,
Email: user.Email,
FirstName: user.FirstName,
LastName: user.LastName,
Avatar: user.Avatar,
Bio: user.Bio,
Location: user.Location,
Birthdate: user.Birthdate,
Gender: user.Gender,
Role: user.Role,
IsActive: user.IsActive,
IsVerified: user.IsVerified,
IsPublic: user.IsPublic,
LastLoginAt: user.LastLoginAt,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
// 2. Récupérer les paramètres utilisateur
var settings models.UserSettings
if err := s.db.WithContext(ctx).First(&settings, "user_id = ?", userID).Error; err == nil {
export.Settings = &UserSettingsExport{
EmailNotifications: settings.EmailNotifications,
PushNotifications: settings.PushNotifications,
BrowserNotifications: settings.BrowserNotifications,
EmailOnFollow: settings.EmailOnFollow,
EmailOnLike: settings.EmailOnLike,
EmailOnComment: settings.EmailOnComment,
EmailOnMessage: settings.EmailOnMessage,
EmailOnMention: settings.EmailOnMention,
EmailMarketing: settings.EmailMarketing,
AllowSearchIndexing: settings.AllowSearchIndexing,
ShowActivity: settings.ShowActivity,
ExplicitContent: settings.ExplicitContent,
Autoplay: settings.Autoplay,
}
}
// 3. Récupérer les tracks de l'utilisateur
var tracks []models.Track
if err := s.db.WithContext(ctx).Where("creator_id = ?", userID).Find(&tracks).Error; err == nil {
export.Tracks = make([]TrackExport, len(tracks))
for i, track := range tracks {
export.Tracks[i] = TrackExport{
ID: track.ID,
Title: track.Title,
Artist: track.Artist,
Album: track.Album,
Duration: track.Duration,
Genre: track.Genre,
Year: track.Year,
FilePath: track.FilePath,
CoverArtPath: track.CoverArtPath,
IsPublic: track.IsPublic,
Status: string(track.Status),
PlayCount: track.PlayCount,
LikeCount: track.LikeCount,
CreatedAt: track.CreatedAt,
UpdatedAt: track.UpdatedAt,
}
}
}
// 4. Récupérer les playlists de l'utilisateur
var playlists []models.Playlist
if err := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&playlists).Error; err == nil {
export.Playlists = make([]PlaylistExport, len(playlists))
for i, playlist := range playlists {
export.Playlists[i] = PlaylistExport{
ID: playlist.ID,
Title: playlist.Title,
Description: playlist.Description,
IsPublic: playlist.IsPublic,
TrackCount: playlist.TrackCount,
FollowerCount: playlist.FollowerCount,
CreatedAt: playlist.CreatedAt,
UpdatedAt: playlist.UpdatedAt,
}
}
}
// 5. Récupérer les commentaires de l'utilisateur
var comments []models.TrackComment
if err := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&comments).Error; err == nil {
export.Comments = make([]CommentExport, len(comments))
for i, comment := range comments {
export.Comments[i] = CommentExport{
ID: comment.ID,
TrackID: comment.TrackID,
Content: comment.Content,
CreatedAt: comment.CreatedAt,
UpdatedAt: comment.UpdatedAt,
}
}
}
// 6. Récupérer les likes de l'utilisateur
var likes []models.TrackLike
if err := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&likes).Error; err == nil {
export.Likes = make([]LikeExport, len(likes))
for i, like := range likes {
export.Likes[i] = LikeExport{
ID: like.ID,
TrackID: like.TrackID,
CreatedAt: like.CreatedAt,
}
}
}
// 7. Récupérer les analytics de l'utilisateur
var analytics []models.PlaybackAnalytics
if err := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&analytics).Error; err == nil {
export.Analytics = make([]AnalyticsExport, len(analytics))
for i, a := range analytics {
export.Analytics[i] = AnalyticsExport{
ID: a.ID,
TrackID: a.TrackID,
PlayTime: a.PlayTime,
PauseCount: a.PauseCount,
SeekCount: a.SeekCount,
CompletionRate: a.CompletionRate,
StartedAt: a.StartedAt,
EndedAt: a.EndedAt,
CreatedAt: a.CreatedAt,
}
}
}
// 8. Récupérer les identités fédérées
var federatedIDs []models.FederatedIdentity
if err := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&federatedIDs).Error; err == nil {
export.FederatedIDs = make([]FederatedIDExport, len(federatedIDs))
for i, fid := range federatedIDs {
export.FederatedIDs[i] = FederatedIDExport{
ID: fid.ID,
Provider: fid.Provider,
ProviderID: fid.ProviderID,
Email: fid.Email,
DisplayName: fid.DisplayName,
CreatedAt: fid.CreatedAt,
}
}
}
// 9. Récupérer les rôles de l'utilisateur
var userRoles []models.UserRole
if err := s.db.WithContext(ctx).Where("user_id = ?", userID).Find(&userRoles).Error; err == nil {
export.Roles = make([]RoleExport, len(userRoles))
for i, ur := range userRoles {
export.Roles[i] = RoleExport{
ID: ur.ID,
RoleName: ur.RoleName,
AssignedAt: ur.AssignedAt,
ExpiresAt: ur.ExpiresAt,
IsActive: ur.IsActive,
}
}
}
s.logger.Info("User data exported",
zap.String("user_id", userID.String()),
zap.Int("tracks", len(export.Tracks)),
zap.Int("playlists", len(export.Playlists)),
zap.Int("comments", len(export.Comments)),
zap.Int("likes", len(export.Likes)),
zap.Int("analytics", len(export.Analytics)),
)
return export, nil
}
// ExportUserDataAsJSON exporte les données utilisateur au format JSON
func (s *DataExportService) ExportUserDataAsJSON(ctx context.Context, userID uuid.UUID) ([]byte, error) {
export, err := s.ExportUserData(ctx, userID)
if err != nil {
return nil, err
}
jsonData, err := json.MarshalIndent(export, "", " ")
if err != nil {
return nil, fmt.Errorf("failed to marshal export data: %w", err)
}
return jsonData, nil
}