A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
780 lines
21 KiB
Go
780 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)
|
|
// ============================================================================
|
|
|
|
// 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
|
|
}
|