veza/veza-backend-api/internal/services/gdpr_export.go
2026-03-05 23:03:43 +01:00

126 lines
3.9 KiB
Go

package services
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"fmt"
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
"veza-backend-api/internal/models"
)
// GDPRExportService handles async GDPR data export with ZIP upload and notification
type GDPRExportService struct {
db *gorm.DB
dataExportService *DataExportService
s3Service *S3StorageService
notificationService *NotificationService
logger *zap.Logger
}
// NewGDPRExportService creates a new GDPR export service
func NewGDPRExportService(
db *gorm.DB,
dataExportService *DataExportService,
s3Service *S3StorageService,
notificationService *NotificationService,
logger *zap.Logger,
) *GDPRExportService {
return &GDPRExportService{
db: db,
dataExportService: dataExportService,
s3Service: s3Service,
notificationService: notificationService,
logger: logger,
}
}
// ExportUserDataAsync runs the export in a goroutine and notifies when ready
func (s *GDPRExportService) ExportUserDataAsync(ctx context.Context, userID uuid.UUID) {
go func() {
exportCtx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
defer cancel()
export, err := s.dataExportService.ExportUserData(exportCtx, userID)
if err != nil {
s.logger.Error("GDPR export failed", zap.Error(err), zap.String("user_id", userID.String()))
if s.notificationService != nil {
_ = s.notificationService.CreateNotification(userID, "export_failed", "Data export failed", "Your data export could not be completed. Please try again.", "")
}
return
}
// Add cloud files metadata
var cloudFiles []models.UserFile
if err := s.db.WithContext(exportCtx).Where("user_id = ?", userID).Find(&cloudFiles).Error; err == nil {
export.CloudFiles = make([]CloudFileExport, len(cloudFiles))
for i, f := range cloudFiles {
export.CloudFiles[i] = CloudFileExport{
ID: f.ID, Filename: f.Filename, SizeBytes: f.SizeBytes, MimeType: f.MimeType, CreatedAt: f.CreatedAt,
}
}
}
// Add gear metadata
var gear []models.GearItem
if err := s.db.WithContext(exportCtx).Where("user_id = ?", userID).Find(&gear).Error; err == nil {
export.Gear = make([]GearExport, len(gear))
for i, g := range gear {
export.Gear[i] = GearExport{
ID: g.ID, Name: g.Name, Category: g.Category, Brand: g.Brand, Model: g.Model, CreatedAt: g.CreatedAt,
}
}
}
jsonData, err := json.MarshalIndent(export, "", " ")
if err != nil {
s.logger.Error("GDPR export marshal failed", zap.Error(err))
return
}
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
w, err := zw.Create("veza-export.json")
if err != nil {
s.logger.Error("GDPR export zip create failed", zap.Error(err))
return
}
if _, err := w.Write(jsonData); err != nil {
zw.Close()
s.logger.Error("GDPR export zip write failed", zap.Error(err))
return
}
if err := zw.Close(); err != nil {
s.logger.Error("GDPR export zip close failed", zap.Error(err))
return
}
key := fmt.Sprintf("exports/%s/veza-export-%s.zip", userID, time.Now().Format("20060102-150405"))
if s.s3Service != nil {
if _, err := s.s3Service.UploadFile(exportCtx, buf.Bytes(), key, "application/zip"); err != nil {
s.logger.Error("GDPR export S3 upload failed", zap.Error(err))
return
}
presignCtx, presignCancel := context.WithTimeout(context.Background(), 10*time.Second)
defer presignCancel()
url, err := s.s3Service.GetPresignedURL(presignCtx, key)
if err != nil {
s.logger.Error("GDPR export presign failed", zap.Error(err))
return
}
if s.notificationService != nil {
_ = s.notificationService.CreateNotification(userID, "export_ready", "Your data export is ready", "Click to download your personal data export (link expires in 1 hour).", url)
}
} else {
s.logger.Warn("GDPR export: S3 not configured, skipping upload")
}
}()
}