2026-02-25 12:35:16 +00:00
package services
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"fmt"
2026-03-10 12:57:04 +00:00
"os"
"strings"
2026-02-25 12:35:16 +00:00
"time"
"github.com/google/uuid"
"go.uber.org/zap"
"gorm.io/gorm"
"veza-backend-api/internal/models"
)
2026-03-10 12:57:04 +00:00
// GDPRExportService handles async GDPR data export with ZIP upload and notification (v0.10.8 F065)
2026-02-25 12:35:16 +00:00
type GDPRExportService struct {
2026-03-05 22:03:43 +00:00
db * gorm . DB
dataExportService * DataExportService
s3Service * S3StorageService
2026-02-25 12:35:16 +00:00
notificationService * NotificationService
2026-03-10 12:57:04 +00:00
emailService EmailServiceInterface // optional, for export_ready email
2026-03-05 22:03:43 +00:00
logger * zap . Logger
2026-02-25 12:35:16 +00:00
}
// 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 ,
}
}
2026-03-10 12:57:04 +00:00
// SetEmailService injects the email service for export-ready notifications
func ( s * GDPRExportService ) SetEmailService ( es EmailServiceInterface ) {
s . emailService = es
}
// ExportUserDataAsync runs the export in a goroutine, stores in data_exports, and notifies when ready
func ( s * GDPRExportService ) ExportUserDataAsync ( ctx context . Context , exportID , userID uuid . UUID ) {
2026-02-25 12:35:16 +00:00
go func ( ) {
exportCtx , cancel := context . WithTimeout ( context . Background ( ) , 15 * time . Minute )
defer cancel ( )
2026-03-10 12:57:04 +00:00
// Update status to processing
s . db . WithContext ( exportCtx ) . Model ( & models . DataExport { } ) . Where ( "id = ? AND user_id = ?" , exportID , userID ) .
Update ( "status" , "processing" )
2026-02-25 12:35:16 +00:00
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 ( ) ) )
2026-03-10 12:57:04 +00:00
errMsg := err . Error ( )
s . db . WithContext ( exportCtx ) . Model ( & models . DataExport { } ) . Where ( "id = ? AND user_id = ?" , exportID , userID ) .
Updates ( map [ string ] interface { } { "status" : "failed" , "error_message" : errMsg , "completed_at" : time . Now ( ) } )
2026-02-25 12:35:16 +00:00
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" ) )
2026-03-10 12:57:04 +00:00
fileSize := int64 ( buf . Len ( ) )
2026-02-25 12:35:16 +00:00
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 ) )
2026-03-10 12:57:04 +00:00
s . db . WithContext ( exportCtx ) . Model ( & models . DataExport { } ) . Where ( "id = ? AND user_id = ?" , exportID , userID ) .
Updates ( map [ string ] interface { } { "status" : "failed" , "error_message" : "S3 upload failed" , "completed_at" : time . Now ( ) } )
2026-02-25 12:35:16 +00:00
return
}
2026-03-10 12:57:04 +00:00
// Update data_export with completed status (v0.10.8)
s . db . WithContext ( exportCtx ) . Model ( & models . DataExport { } ) . Where ( "id = ? AND user_id = ?" , exportID , userID ) .
Updates ( map [ string ] interface { } {
"status" : "completed" ,
"s3_key" : key ,
"file_size_bytes" : fileSize ,
"completed_at" : time . Now ( ) ,
} )
// Download: link to frontend settings (user must be logged in to download via API)
frontendURL := getFrontendURL ( )
settingsURL := fmt . Sprintf ( "%s/settings?tab=export" , frontendURL )
2026-02-25 12:35:16 +00:00
if s . notificationService != nil {
2026-03-10 12:57:04 +00:00
_ = s . notificationService . CreateNotification ( userID , "export_ready" , "Your data export is ready" , "Go to Settings to download your personal data (valid 7 days)." , settingsURL )
}
if s . emailService != nil && export . Profile != nil && export . Profile . Email != "" {
msg := fmt . Sprintf ( "Your Veza data export is ready. Go to your account settings to download it (valid for 7 days): %s" , settingsURL )
_ = s . emailService . SendNotificationEmail ( export . Profile . Email , "Your Veza data export is ready" , msg , "data_export" )
2026-02-25 12:35:16 +00:00
}
} else {
s . logger . Warn ( "GDPR export: S3 not configured, skipping upload" )
2026-03-10 12:57:04 +00:00
s . db . WithContext ( exportCtx ) . Model ( & models . DataExport { } ) . Where ( "id = ? AND user_id = ?" , exportID , userID ) .
Updates ( map [ string ] interface { } { "status" : "failed" , "error_message" : "S3 not configured" , "completed_at" : time . Now ( ) } )
2026-02-25 12:35:16 +00:00
}
} ( )
}
2026-03-10 12:57:04 +00:00
func getFrontendURL ( ) string {
if u := os . Getenv ( "FRONTEND_URL" ) ; u != "" {
return strings . TrimSuffix ( u , "/" )
}
if u := os . Getenv ( "VITE_FRONTEND_URL" ) ; u != "" {
return strings . TrimSuffix ( u , "/" )
}
return "http://localhost:5173"
}