Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/dsgvo_handlers.go
Benjamin Admin 3593a4ff78
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 33s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 15s
feat(tom): TOM-Backend in Python erstellen, Frontend von In-Memory auf DB migrieren
- Migration 034: compliance_tom_state + compliance_tom_measures Tabellen
- Python Routes: State CRUD, Measures CRUD, Bulk-Upsert, Stats, CSV/JSON-Export
- Frontend-Proxy: In-Memory Storage durch Proxy zu backend-compliance ersetzt
- Go TOM-Handler als DEPRECATED markiert (Source of Truth ist jetzt Python)
- 44 Tests (alle bestanden)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:35:44 +01:00

787 lines
21 KiB
Go

package handlers
import (
"bytes"
"fmt"
"net/http"
"time"
"github.com/breakpilot/ai-compliance-sdk/internal/dsgvo"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// DSGVOHandlers handles DSGVO-related API endpoints
type DSGVOHandlers struct {
store *dsgvo.Store
}
// NewDSGVOHandlers creates new DSGVO handlers
func NewDSGVOHandlers(store *dsgvo.Store) *DSGVOHandlers {
return &DSGVOHandlers{store: store}
}
// ============================================================================
// VVT - Verarbeitungsverzeichnis (Processing Activities)
// DEPRECATED: VVT is now managed by backend-compliance (Python).
// These handlers will be removed once all DSGVO sub-modules are consolidated.
// ============================================================================
// ListProcessingActivities returns all processing activities for a tenant
func (h *DSGVOHandlers) ListProcessingActivities(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var namespaceID *uuid.UUID
if nsID := c.Query("namespace_id"); nsID != "" {
if id, err := uuid.Parse(nsID); err == nil {
namespaceID = &id
}
}
activities, err := h.store.ListProcessingActivities(c.Request.Context(), tenantID, namespaceID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"processing_activities": activities})
}
// GetProcessingActivity returns a processing activity by ID
func (h *DSGVOHandlers) GetProcessingActivity(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
activity, err := h.store.GetProcessingActivity(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if activity == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, activity)
}
// CreateProcessingActivity creates a new processing activity
func (h *DSGVOHandlers) CreateProcessingActivity(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var activity dsgvo.ProcessingActivity
if err := c.ShouldBindJSON(&activity); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
activity.TenantID = tenantID
activity.CreatedBy = userID
if activity.Status == "" {
activity.Status = "draft"
}
if err := h.store.CreateProcessingActivity(c.Request.Context(), &activity); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, activity)
}
// UpdateProcessingActivity updates a processing activity
func (h *DSGVOHandlers) UpdateProcessingActivity(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
var activity dsgvo.ProcessingActivity
if err := c.ShouldBindJSON(&activity); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
activity.ID = id
if err := h.store.UpdateProcessingActivity(c.Request.Context(), &activity); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, activity)
}
// DeleteProcessingActivity deletes a processing activity
func (h *DSGVOHandlers) DeleteProcessingActivity(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
if err := h.store.DeleteProcessingActivity(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ============================================================================
// TOM - Technische und Organisatorische Maßnahmen
// ============================================================================
// DEPRECATED: TOM is now managed by backend-compliance (Python).
// These handlers remain for backwards compatibility but should not be used.
// Use backend-compliance endpoints: GET/POST /api/compliance/tom/...
// ListTOMs returns all TOMs for a tenant
// DEPRECATED: Use backend-compliance GET /api/compliance/tom/measures
func (h *DSGVOHandlers) ListTOMs(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
category := c.Query("category")
toms, err := h.store.ListTOMs(c.Request.Context(), tenantID, category)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"toms": toms, "categories": dsgvo.TOMCategories})
}
// GetTOM returns a TOM by ID
func (h *DSGVOHandlers) GetTOM(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
tom, err := h.store.GetTOM(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if tom == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, tom)
}
// CreateTOM creates a new TOM
func (h *DSGVOHandlers) CreateTOM(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var tom dsgvo.TOM
if err := c.ShouldBindJSON(&tom); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
tom.TenantID = tenantID
tom.CreatedBy = userID
if tom.ImplementationStatus == "" {
tom.ImplementationStatus = "planned"
}
if err := h.store.CreateTOM(c.Request.Context(), &tom); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, tom)
}
// ============================================================================
// DSR - Data Subject Requests
// ============================================================================
// ListDSRs returns all DSRs for a tenant
func (h *DSGVOHandlers) ListDSRs(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
status := c.Query("status")
requestType := c.Query("type")
dsrs, err := h.store.ListDSRs(c.Request.Context(), tenantID, status, requestType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"dsrs": dsrs, "types": dsgvo.DSRTypes})
}
// GetDSR returns a DSR by ID
func (h *DSGVOHandlers) GetDSR(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
dsr, err := h.store.GetDSR(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if dsr == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, dsr)
}
// CreateDSR creates a new DSR
func (h *DSGVOHandlers) CreateDSR(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var dsr dsgvo.DSR
if err := c.ShouldBindJSON(&dsr); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dsr.TenantID = tenantID
dsr.CreatedBy = userID
if dsr.Status == "" {
dsr.Status = "received"
}
if err := h.store.CreateDSR(c.Request.Context(), &dsr); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, dsr)
}
// UpdateDSR updates a DSR
func (h *DSGVOHandlers) UpdateDSR(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
var dsr dsgvo.DSR
if err := c.ShouldBindJSON(&dsr); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dsr.ID = id
if err := h.store.UpdateDSR(c.Request.Context(), &dsr); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, dsr)
}
// ============================================================================
// Retention Policies
// ============================================================================
// ListRetentionPolicies returns all retention policies for a tenant
func (h *DSGVOHandlers) ListRetentionPolicies(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
policies, err := h.store.ListRetentionPolicies(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"policies": policies,
"common_periods": dsgvo.CommonRetentionPeriods,
})
}
// CreateRetentionPolicy creates a new retention policy
func (h *DSGVOHandlers) CreateRetentionPolicy(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var policy dsgvo.RetentionPolicy
if err := c.ShouldBindJSON(&policy); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
policy.TenantID = tenantID
policy.CreatedBy = userID
if policy.Status == "" {
policy.Status = "draft"
}
if err := h.store.CreateRetentionPolicy(c.Request.Context(), &policy); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, policy)
}
// ============================================================================
// Statistics
// ============================================================================
// GetStats returns DSGVO statistics for a tenant
func (h *DSGVOHandlers) GetStats(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
stats, err := h.store.GetStats(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
// ============================================================================
// DSFA - Datenschutz-Folgenabschätzung
// ============================================================================
// ListDSFAs returns all DSFAs for a tenant
func (h *DSGVOHandlers) ListDSFAs(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
status := c.Query("status")
dsfas, err := h.store.ListDSFAs(c.Request.Context(), tenantID, status)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"dsfas": dsfas})
}
// GetDSFA returns a DSFA by ID
func (h *DSGVOHandlers) GetDSFA(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
dsfa, err := h.store.GetDSFA(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if dsfa == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusOK, dsfa)
}
// CreateDSFA creates a new DSFA
func (h *DSGVOHandlers) CreateDSFA(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
userID := rbac.GetUserID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
var dsfa dsgvo.DSFA
if err := c.ShouldBindJSON(&dsfa); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dsfa.TenantID = tenantID
dsfa.CreatedBy = userID
if dsfa.Status == "" {
dsfa.Status = "draft"
}
if err := h.store.CreateDSFA(c.Request.Context(), &dsfa); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, dsfa)
}
// UpdateDSFA updates a DSFA
func (h *DSGVOHandlers) UpdateDSFA(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
var dsfa dsgvo.DSFA
if err := c.ShouldBindJSON(&dsfa); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
dsfa.ID = id
if err := h.store.UpdateDSFA(c.Request.Context(), &dsfa); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, dsfa)
}
// DeleteDSFA deletes a DSFA
func (h *DSGVOHandlers) DeleteDSFA(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
if err := h.store.DeleteDSFA(c.Request.Context(), id); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
}
// ============================================================================
// PDF Export
// ============================================================================
// ExportVVT exports the Verarbeitungsverzeichnis as CSV/JSON
func (h *DSGVOHandlers) ExportVVT(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
format := c.DefaultQuery("format", "csv")
activities, err := h.store.ListProcessingActivities(c.Request.Context(), tenantID, nil)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if format == "json" {
c.Header("Content-Disposition", "attachment; filename=vvt_export.json")
c.JSON(http.StatusOK, gin.H{
"exported_at": time.Now().UTC().Format(time.RFC3339),
"processing_activities": activities,
})
return
}
// CSV Export
var buf bytes.Buffer
buf.WriteString("ID;Name;Zweck;Rechtsgrundlage;Datenkategorien;Betroffene;Empfänger;Drittland;Aufbewahrung;Verantwortlich;Status;Erstellt\n")
for _, pa := range activities {
buf.WriteString(fmt.Sprintf("%s;%s;%s;%s;%s;%s;%s;%t;%s;%s;%s;%s\n",
pa.ID.String(),
escapeCSV(pa.Name),
escapeCSV(pa.Purpose),
pa.LegalBasis,
joinStrings(pa.DataCategories),
joinStrings(pa.DataSubjectCategories),
joinStrings(pa.Recipients),
pa.ThirdCountryTransfer,
pa.RetentionPeriod,
escapeCSV(pa.ResponsiblePerson),
pa.Status,
pa.CreatedAt.Format("2006-01-02"),
))
}
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=vvt_export.csv")
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
}
// ExportTOM exports the TOM catalog as CSV/JSON
// DEPRECATED: Use backend-compliance GET /api/compliance/tom/export
func (h *DSGVOHandlers) ExportTOM(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
format := c.DefaultQuery("format", "csv")
toms, err := h.store.ListTOMs(c.Request.Context(), tenantID, "")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if format == "json" {
c.Header("Content-Disposition", "attachment; filename=tom_export.json")
c.JSON(http.StatusOK, gin.H{
"exported_at": time.Now().UTC().Format(time.RFC3339),
"toms": toms,
"categories": dsgvo.TOMCategories,
})
return
}
// CSV Export
var buf bytes.Buffer
buf.WriteString("ID;Kategorie;Name;Beschreibung;Typ;Status;Implementiert am;Verantwortlich;Wirksamkeit;Erstellt\n")
for _, tom := range toms {
implementedAt := ""
if tom.ImplementedAt != nil {
implementedAt = tom.ImplementedAt.Format("2006-01-02")
}
buf.WriteString(fmt.Sprintf("%s;%s;%s;%s;%s;%s;%s;%s;%s;%s\n",
tom.ID.String(),
tom.Category,
escapeCSV(tom.Name),
escapeCSV(tom.Description),
tom.Type,
tom.ImplementationStatus,
implementedAt,
escapeCSV(tom.ResponsiblePerson),
tom.EffectivenessRating,
tom.CreatedAt.Format("2006-01-02"),
))
}
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=tom_export.csv")
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
}
// ExportDSR exports DSR overview as CSV/JSON
func (h *DSGVOHandlers) ExportDSR(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
format := c.DefaultQuery("format", "csv")
dsrs, err := h.store.ListDSRs(c.Request.Context(), tenantID, "", "")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if format == "json" {
c.Header("Content-Disposition", "attachment; filename=dsr_export.json")
c.JSON(http.StatusOK, gin.H{
"exported_at": time.Now().UTC().Format(time.RFC3339),
"dsrs": dsrs,
"types": dsgvo.DSRTypes,
})
return
}
// CSV Export
var buf bytes.Buffer
buf.WriteString("ID;Typ;Name;E-Mail;Status;Eingegangen;Frist;Abgeschlossen;Kanal;Zugewiesen\n")
for _, dsr := range dsrs {
completedAt := ""
if dsr.CompletedAt != nil {
completedAt = dsr.CompletedAt.Format("2006-01-02")
}
assignedTo := ""
if dsr.AssignedTo != nil {
assignedTo = dsr.AssignedTo.String()
}
buf.WriteString(fmt.Sprintf("%s;%s;%s;%s;%s;%s;%s;%s;%s;%s\n",
dsr.ID.String(),
dsr.RequestType,
escapeCSV(dsr.SubjectName),
dsr.SubjectEmail,
dsr.Status,
dsr.ReceivedAt.Format("2006-01-02"),
dsr.DeadlineAt.Format("2006-01-02"),
completedAt,
dsr.RequestChannel,
assignedTo,
))
}
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=dsr_export.csv")
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
}
// ExportDSFA exports a DSFA as JSON
func (h *DSGVOHandlers) ExportDSFA(c *gin.Context) {
id, err := uuid.Parse(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
return
}
dsfa, err := h.store.GetDSFA(c.Request.Context(), id)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if dsfa == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=dsfa_%s.json", id.String()[:8]))
c.JSON(http.StatusOK, gin.H{
"exported_at": time.Now().UTC().Format(time.RFC3339),
"dsfa": dsfa,
})
}
// ExportRetentionPolicies exports retention policies as CSV/JSON
func (h *DSGVOHandlers) ExportRetentionPolicies(c *gin.Context) {
tenantID := rbac.GetTenantID(c)
if tenantID == uuid.Nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
return
}
format := c.DefaultQuery("format", "csv")
policies, err := h.store.ListRetentionPolicies(c.Request.Context(), tenantID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if format == "json" {
c.Header("Content-Disposition", "attachment; filename=retention_policies_export.json")
c.JSON(http.StatusOK, gin.H{
"exported_at": time.Now().UTC().Format(time.RFC3339),
"policies": policies,
"common_periods": dsgvo.CommonRetentionPeriods,
})
return
}
// CSV Export
var buf bytes.Buffer
buf.WriteString("ID;Name;Datenkategorie;Aufbewahrungsdauer (Tage);Dauer (Text);Rechtsgrundlage;Referenz;Löschmethode;Status\n")
for _, rp := range policies {
buf.WriteString(fmt.Sprintf("%s;%s;%s;%d;%s;%s;%s;%s;%s\n",
rp.ID.String(),
escapeCSV(rp.Name),
rp.DataCategory,
rp.RetentionPeriodDays,
escapeCSV(rp.RetentionPeriodText),
escapeCSV(rp.LegalBasis),
escapeCSV(rp.LegalReference),
rp.DeletionMethod,
rp.Status,
))
}
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=retention_policies_export.csv")
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
}
// Helper functions
func escapeCSV(s string) string {
// Simple CSV escaping - wrap in quotes if contains semicolon, quote, or newline
if s == "" {
return ""
}
needsQuotes := false
for _, c := range s {
if c == ';' || c == '"' || c == '\n' || c == '\r' {
needsQuotes = true
break
}
}
if needsQuotes {
// Double any quotes and wrap in quotes
escaped := ""
for _, c := range s {
if c == '"' {
escaped += "\"\""
} else if c == '\n' || c == '\r' {
escaped += " "
} else {
escaped += string(c)
}
}
return "\"" + escaped + "\""
}
return s
}
func joinStrings(strs []string) string {
if len(strs) == 0 {
return ""
}
result := strs[0]
for i := 1; i < len(strs); i++ {
result += ", " + strs[i]
}
return result
}