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) // ============================================================================ // 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 // ============================================================================ // ListTOMs returns all TOMs for a tenant 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 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 }