[split-required] [guardrail-change] Enforce 500 LOC budget across all services

Install LOC guardrails (check-loc.sh, architecture.md, pre-commit hook)
and split all 44 files exceeding 500 LOC into domain-focused modules:

- consent-service (Go): models, handlers, services, database splits
- backend-core (Python): security_api, rbac_api, pdf_service, auth splits
- admin-core (TypeScript): 5 page.tsx + sidebar extractions
- pitch-deck (TypeScript): 6 slides, 3 UI components, engine.ts splits
- voice-service (Python): enhanced_task_orchestrator split

Result: 0 violations, 36 exempted (pipeline, tests, pure-data files).
Go build verified clean. No behavior changes — pure structural splits.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-27 00:09:30 +02:00
parent 5ef039a6bc
commit 92c86ec6ba
162 changed files with 23853 additions and 23034 deletions

View File

@@ -2,11 +2,8 @@ package handlers
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
@@ -308,254 +305,3 @@ func (h *Handler) RevokeBannerConsent(c *gin.Context) {
"revokedAt": time.Now().UTC().Format(time.RFC3339),
})
}
// GetSiteConfig gibt die Konfiguration für eine Site zurück
// GET /api/v1/banner/config/:siteId
func (h *Handler) GetSiteConfig(c *gin.Context) {
siteID := c.Param("siteId")
// Standard-Kategorien (aus Datenbank oder Default)
categories := []CategoryConfig{
{
ID: "essential",
Name: map[string]string{
"de": "Essentiell",
"en": "Essential",
},
Description: map[string]string{
"de": "Notwendig für die Grundfunktionen der Website.",
"en": "Required for basic website functionality.",
},
Required: true,
Vendors: []VendorConfig{},
},
{
ID: "functional",
Name: map[string]string{
"de": "Funktional",
"en": "Functional",
},
Description: map[string]string{
"de": "Ermöglicht Personalisierung und Komfortfunktionen.",
"en": "Enables personalization and comfort features.",
},
Required: false,
Vendors: []VendorConfig{},
},
{
ID: "analytics",
Name: map[string]string{
"de": "Statistik",
"en": "Analytics",
},
Description: map[string]string{
"de": "Hilft uns, die Website zu verbessern.",
"en": "Helps us improve the website.",
},
Required: false,
Vendors: []VendorConfig{},
},
{
ID: "marketing",
Name: map[string]string{
"de": "Marketing",
"en": "Marketing",
},
Description: map[string]string{
"de": "Ermöglicht personalisierte Werbung.",
"en": "Enables personalized advertising.",
},
Required: false,
Vendors: []VendorConfig{},
},
{
ID: "social",
Name: map[string]string{
"de": "Soziale Medien",
"en": "Social Media",
},
Description: map[string]string{
"de": "Ermöglicht Inhalte von sozialen Netzwerken.",
"en": "Enables content from social networks.",
},
Required: false,
Vendors: []VendorConfig{},
},
}
config := SiteConfig{
SiteID: siteID,
SiteName: "BreakPilot",
Categories: categories,
UI: UIConfig{
Theme: "auto",
Position: "bottom",
},
Legal: LegalConfig{
PrivacyPolicyURL: "/datenschutz",
ImprintURL: "/impressum",
},
}
c.JSON(http.StatusOK, config)
}
// ExportBannerConsent exportiert alle Consent-Daten eines Nutzers (DSGVO Art. 20)
// GET /api/v1/banner/consent/export?userId=xxx
func (h *Handler) ExportBannerConsent(c *gin.Context) {
userID := c.Query("userId")
if userID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"error": "missing_user_id",
"message": "userId parameter is required",
})
return
}
ctx := context.Background()
rows, err := h.db.Pool.Query(ctx, `
SELECT id, site_id, device_fingerprint, categories, vendors,
version, created_at, updated_at, revoked_at
FROM banner_consents
WHERE user_id = $1
ORDER BY created_at DESC
`, userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": "export_failed",
"message": "Failed to export consent data",
})
return
}
defer rows.Close()
var consents []map[string]interface{}
for rows.Next() {
var id, siteID, deviceFingerprint, version string
var categoriesJSON, vendorsJSON []byte
var createdAt, updatedAt time.Time
var revokedAt *time.Time
rows.Scan(&id, &siteID, &deviceFingerprint, &categoriesJSON, &vendorsJSON,
&version, &createdAt, &updatedAt, &revokedAt)
var categories, vendors map[string]bool
json.Unmarshal(categoriesJSON, &categories)
json.Unmarshal(vendorsJSON, &vendors)
consent := map[string]interface{}{
"consentId": id,
"siteId": siteID,
"consent": map[string]interface{}{
"categories": categories,
"vendors": vendors,
},
"createdAt": createdAt.UTC().Format(time.RFC3339),
"revokedAt": nil,
}
if revokedAt != nil {
consent["revokedAt"] = revokedAt.UTC().Format(time.RFC3339)
}
consents = append(consents, consent)
}
c.JSON(http.StatusOK, gin.H{
"userId": userID,
"exportedAt": time.Now().UTC().Format(time.RFC3339),
"consents": consents,
})
}
// GetBannerStats gibt anonymisierte Statistiken zurück (Admin)
// GET /api/v1/banner/admin/stats/:siteId
func (h *Handler) GetBannerStats(c *gin.Context) {
siteID := c.Param("siteId")
ctx := context.Background()
// Gesamtanzahl Consents
var totalConsents int
h.db.Pool.QueryRow(ctx, `
SELECT COUNT(*) FROM banner_consents
WHERE site_id = $1 AND revoked_at IS NULL
`, siteID).Scan(&totalConsents)
// Consent-Rate pro Kategorie
categoryStats := make(map[string]map[string]interface{})
rows, _ := h.db.Pool.Query(ctx, `
SELECT
key as category,
COUNT(*) FILTER (WHERE value::text = 'true') as accepted,
COUNT(*) as total
FROM banner_consents,
jsonb_each(categories::jsonb)
WHERE site_id = $1 AND revoked_at IS NULL
GROUP BY key
`, siteID)
if rows != nil {
defer rows.Close()
for rows.Next() {
var category string
var accepted, total int
rows.Scan(&category, &accepted, &total)
rate := float64(0)
if total > 0 {
rate = float64(accepted) / float64(total)
}
categoryStats[category] = map[string]interface{}{
"accepted": accepted,
"rate": rate,
}
}
}
c.JSON(http.StatusOK, gin.H{
"siteId": siteID,
"period": gin.H{
"from": time.Now().AddDate(0, -1, 0).Format("2006-01-02"),
"to": time.Now().Format("2006-01-02"),
},
"totalConsents": totalConsents,
"consentByCategory": categoryStats,
})
}
// ========================================
// Helper Functions
// ========================================
// anonymizeIP anonymisiert eine IP-Adresse (DSGVO-konform)
func anonymizeIP(ip string) string {
// IPv4: Letztes Oktett auf 0
parts := strings.Split(ip, ".")
if len(parts) == 4 {
parts[3] = "0"
anonymized := strings.Join(parts, ".")
hash := sha256.Sum256([]byte(anonymized))
return hex.EncodeToString(hash[:])[:16]
}
// IPv6: Hash
hash := sha256.Sum256([]byte(ip))
return hex.EncodeToString(hash[:])[:16]
}
// logBannerConsentAudit schreibt einen Audit-Log-Eintrag
func (h *Handler) logBannerConsentAudit(ctx context.Context, consentID, action string, req interface{}, ipHash string) {
details, _ := json.Marshal(req)
h.db.Pool.Exec(ctx, `
INSERT INTO banner_consent_audit_log (
id, consent_id, action, details, ip_hash, created_at
) VALUES ($1, $2, $3, $4, $5, NOW())
`, uuid.New().String(), consentID, action, string(details), ipHash)
}