Files
breakpilot-core/consent-service/cmd/server/main.go
Benjamin Boenisch ad111d5e69 Initial commit: breakpilot-core - Shared Infrastructure
Docker Compose with 24+ services:
- PostgreSQL (PostGIS), Valkey, MinIO, Qdrant
- Vault (PKI/TLS), Nginx (Reverse Proxy)
- Backend Core API, Consent Service, Billing Service
- RAG Service, Embedding Service
- Gitea, Woodpecker CI/CD
- Night Scheduler, Health Aggregator
- Jitsi (Web/XMPP/JVB/Jicofo), Mailpit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 23:47:13 +01:00

472 lines
18 KiB
Go

package main
import (
"context"
"fmt"
"log"
"time"
"github.com/breakpilot/consent-service/internal/config"
"github.com/breakpilot/consent-service/internal/database"
"github.com/breakpilot/consent-service/internal/handlers"
"github.com/breakpilot/consent-service/internal/middleware"
"github.com/breakpilot/consent-service/internal/services"
"github.com/breakpilot/consent-service/internal/services/jitsi"
"github.com/breakpilot/consent-service/internal/services/matrix"
"github.com/gin-gonic/gin"
)
func main() {
// Load configuration
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// Initialize database
db, err := database.Connect(cfg.DatabaseURL)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer db.Close()
// Run migrations
if err := database.Migrate(db); err != nil {
log.Fatalf("Failed to run migrations: %v", err)
}
// Setup Gin router
if cfg.Environment == "production" {
gin.SetMode(gin.ReleaseMode)
}
router := gin.Default()
// Global middleware
router.Use(middleware.CORS())
router.Use(middleware.RequestLogger())
router.Use(middleware.RateLimiter())
// Health check
router.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{
"status": "healthy",
"service": "consent-service",
"version": "1.0.0",
})
})
// Initialize services
authService := services.NewAuthService(db.Pool, cfg.JWTSecret, cfg.JWTRefreshSecret)
oauthService := services.NewOAuthService(db.Pool, cfg.JWTSecret)
totpService := services.NewTOTPService(db.Pool, "BreakPilot")
emailService := services.NewEmailService(services.EmailConfig{
Host: cfg.SMTPHost,
Port: cfg.SMTPPort,
Username: cfg.SMTPUsername,
Password: cfg.SMTPPassword,
FromName: cfg.SMTPFromName,
FromAddr: cfg.SMTPFromAddr,
BaseURL: cfg.FrontendURL,
})
notificationService := services.NewNotificationService(db.Pool, emailService)
deadlineService := services.NewDeadlineService(db.Pool, notificationService)
emailTemplateService := services.NewEmailTemplateService(db.Pool)
dsrService := services.NewDSRService(db.Pool, notificationService, emailService)
// Initialize handlers
h := handlers.New(db)
authHandler := handlers.NewAuthHandler(authService, emailService)
oauthHandler := handlers.NewOAuthHandler(oauthService, totpService, authService)
notificationHandler := handlers.NewNotificationHandler(notificationService)
deadlineHandler := handlers.NewDeadlineHandler(deadlineService)
emailTemplateHandler := handlers.NewEmailTemplateHandler(emailTemplateService)
dsrHandler := handlers.NewDSRHandler(dsrService)
// Initialize Matrix service (if enabled)
var matrixService *matrix.MatrixService
if cfg.MatrixEnabled && cfg.MatrixAccessToken != "" {
matrixService = matrix.NewMatrixService(matrix.Config{
HomeserverURL: cfg.MatrixHomeserverURL,
AccessToken: cfg.MatrixAccessToken,
ServerName: cfg.MatrixServerName,
})
log.Println("Matrix service initialized")
} else {
log.Println("Matrix service disabled or not configured")
}
// Initialize Jitsi service (if enabled)
var jitsiService *jitsi.JitsiService
if cfg.JitsiEnabled {
jitsiService = jitsi.NewJitsiService(jitsi.Config{
BaseURL: cfg.JitsiBaseURL,
AppID: cfg.JitsiAppID,
AppSecret: cfg.JitsiAppSecret,
})
log.Println("Jitsi service initialized")
} else {
log.Println("Jitsi service disabled")
}
// Initialize communication handlers
communicationHandler := handlers.NewCommunicationHandlers(matrixService, jitsiService)
// Initialize default email templates (runs only once)
if err := emailTemplateService.InitDefaultTemplates(context.Background()); err != nil {
log.Printf("Warning: Failed to initialize default email templates: %v", err)
}
// API v1 routes
v1 := router.Group("/api/v1")
{
// =============================================
// OAuth 2.0 Endpoints (RFC 6749)
// =============================================
oauth := v1.Group("/oauth")
{
// Authorization endpoint (requires user auth for consent)
oauth.GET("/authorize", middleware.AuthMiddleware(cfg.JWTSecret), oauthHandler.Authorize)
// Token endpoint (public)
oauth.POST("/token", oauthHandler.Token)
// Revocation endpoint (RFC 7009)
oauth.POST("/revoke", oauthHandler.Revoke)
// Introspection endpoint (RFC 7662)
oauth.POST("/introspect", oauthHandler.Introspect)
}
// =============================================
// Authentication Routes (with 2FA support)
// =============================================
auth := v1.Group("/auth")
{
// Registration with mandatory 2FA setup
auth.POST("/register", oauthHandler.RegisterWith2FA)
// Login with 2FA support
auth.POST("/login", oauthHandler.LoginWith2FA)
// 2FA challenge verification (during login)
auth.POST("/2fa/verify", oauthHandler.Verify2FAChallenge)
// Legacy endpoints (kept for compatibility)
auth.POST("/logout", authHandler.Logout)
auth.POST("/refresh", authHandler.RefreshToken)
auth.POST("/verify-email", authHandler.VerifyEmail)
auth.POST("/resend-verification", authHandler.ResendVerification)
auth.POST("/forgot-password", authHandler.ForgotPassword)
auth.POST("/reset-password", authHandler.ResetPassword)
}
// =============================================
// 2FA Management Routes (require auth)
// =============================================
twoFA := v1.Group("/auth/2fa")
twoFA.Use(middleware.AuthMiddleware(cfg.JWTSecret))
{
twoFA.GET("/status", oauthHandler.Get2FAStatus)
twoFA.POST("/setup", oauthHandler.Setup2FA)
twoFA.POST("/verify-setup", oauthHandler.Verify2FASetup)
twoFA.POST("/disable", oauthHandler.Disable2FA)
twoFA.POST("/recovery-codes", oauthHandler.RegenerateRecoveryCodes)
}
// =============================================
// Profile Routes (require auth)
// =============================================
profile := v1.Group("/profile")
profile.Use(middleware.AuthMiddleware(cfg.JWTSecret))
{
profile.GET("", authHandler.GetProfile)
profile.PUT("", authHandler.UpdateProfile)
profile.PUT("/password", authHandler.ChangePassword)
profile.GET("/sessions", authHandler.GetActiveSessions)
profile.DELETE("/sessions/:id", authHandler.RevokeSession)
}
// =============================================
// Public consent routes (require user auth)
// =============================================
public := v1.Group("")
public.Use(middleware.AuthMiddleware(cfg.JWTSecret))
{
// Documents
public.GET("/documents", h.GetDocuments)
public.GET("/documents/:type", h.GetDocumentByType)
public.GET("/documents/:type/latest", h.GetLatestDocumentVersion)
// User Consent
public.POST("/consent", h.CreateConsent)
public.GET("/consent/my", h.GetMyConsents)
public.GET("/consent/check/:documentType", h.CheckConsent)
public.DELETE("/consent/:id", h.WithdrawConsent)
// Cookie Consent
public.GET("/cookies/categories", h.GetCookieCategories)
public.POST("/cookies/consent", h.SetCookieConsent)
public.GET("/cookies/consent/my", h.GetMyCookieConsent)
// GDPR / Data Subject Rights
public.GET("/privacy/my-data", h.GetMyData)
public.POST("/privacy/export", h.RequestDataExport)
public.POST("/privacy/delete", h.RequestDataDeletion)
// Data Subject Requests (User-facing)
public.POST("/dsr", dsrHandler.CreateDSR)
public.GET("/dsr", dsrHandler.GetMyDSRs)
public.GET("/dsr/:id", dsrHandler.GetMyDSR)
public.POST("/dsr/:id/cancel", dsrHandler.CancelMyDSR)
// Notifications
public.GET("/notifications", notificationHandler.GetNotifications)
public.GET("/notifications/unread-count", notificationHandler.GetUnreadCount)
public.PUT("/notifications/:id/read", notificationHandler.MarkAsRead)
public.PUT("/notifications/read-all", notificationHandler.MarkAllAsRead)
public.DELETE("/notifications/:id", notificationHandler.DeleteNotification)
public.GET("/notifications/preferences", notificationHandler.GetPreferences)
public.PUT("/notifications/preferences", notificationHandler.UpdatePreferences)
// Consent Deadlines & Suspension Status
public.GET("/consent/deadlines", deadlineHandler.GetPendingDeadlines)
public.GET("/account/suspension-status", deadlineHandler.GetSuspensionStatus)
}
// Admin routes (require admin auth)
admin := v1.Group("/admin")
admin.Use(middleware.AuthMiddleware(cfg.JWTSecret))
admin.Use(middleware.AdminOnly())
{
// Document Management
admin.GET("/documents", h.AdminGetDocuments)
admin.POST("/documents", h.AdminCreateDocument)
admin.PUT("/documents/:id", h.AdminUpdateDocument)
admin.DELETE("/documents/:id", h.AdminDeleteDocument)
admin.GET("/documents/:docId/versions", h.AdminGetVersions)
// Version Management
admin.POST("/versions", h.AdminCreateVersion)
admin.PUT("/versions/:id", h.AdminUpdateVersion)
admin.DELETE("/versions/:id", h.AdminDeleteVersion)
admin.POST("/versions/:id/archive", h.AdminArchiveVersion)
admin.POST("/versions/:id/submit-review", h.AdminSubmitForReview)
admin.POST("/versions/:id/approve", h.AdminApproveVersion)
admin.POST("/versions/:id/reject", h.AdminRejectVersion)
admin.GET("/versions/:id/compare", h.AdminCompareVersions)
admin.GET("/versions/:id/approval-history", h.AdminGetApprovalHistory)
// Publishing (DSB role recommended but Admin can also do it in dev)
admin.POST("/versions/:id/publish", h.AdminPublishVersion)
// Cookie Categories
admin.GET("/cookies/categories", h.AdminGetCookieCategories)
admin.POST("/cookies/categories", h.AdminCreateCookieCategory)
admin.PUT("/cookies/categories/:id", h.AdminUpdateCookieCategory)
admin.DELETE("/cookies/categories/:id", h.AdminDeleteCookieCategory)
// Statistics & Audit
admin.GET("/stats/consents", h.GetConsentStats)
admin.GET("/stats/cookies", h.GetCookieStats)
admin.GET("/audit-log", h.GetAuditLog)
// Deadline Management (for testing/manual trigger)
admin.POST("/deadlines/process", deadlineHandler.TriggerDeadlineProcessing)
// Scheduled Publishing
admin.GET("/scheduled-versions", h.GetScheduledVersions)
admin.POST("/scheduled-publishing/process", h.ProcessScheduledPublishing)
// OAuth Client Management
admin.GET("/oauth/clients", oauthHandler.AdminGetClients)
admin.POST("/oauth/clients", oauthHandler.AdminCreateClient)
// =============================================
// E-Mail Template Management
// =============================================
admin.GET("/email-templates/types", emailTemplateHandler.GetAllTemplateTypes)
admin.GET("/email-templates", emailTemplateHandler.GetAllTemplates)
admin.GET("/email-templates/settings", emailTemplateHandler.GetSettings)
admin.PUT("/email-templates/settings", emailTemplateHandler.UpdateSettings)
admin.GET("/email-templates/stats", emailTemplateHandler.GetEmailStats)
admin.GET("/email-templates/logs", emailTemplateHandler.GetSendLogs)
admin.GET("/email-templates/default/:type", emailTemplateHandler.GetDefaultContent)
admin.POST("/email-templates/initialize", emailTemplateHandler.InitializeTemplates)
admin.GET("/email-templates/:id", emailTemplateHandler.GetTemplate)
admin.POST("/email-templates", emailTemplateHandler.CreateTemplate)
admin.GET("/email-templates/:id/versions", emailTemplateHandler.GetTemplateVersions)
// E-Mail Template Versions
admin.GET("/email-template-versions/:id", emailTemplateHandler.GetVersion)
admin.POST("/email-template-versions", emailTemplateHandler.CreateVersion)
admin.PUT("/email-template-versions/:id", emailTemplateHandler.UpdateVersion)
admin.POST("/email-template-versions/:id/submit", emailTemplateHandler.SubmitForReview)
admin.POST("/email-template-versions/:id/approve", emailTemplateHandler.ApproveVersion)
admin.POST("/email-template-versions/:id/reject", emailTemplateHandler.RejectVersion)
admin.POST("/email-template-versions/:id/publish", emailTemplateHandler.PublishVersion)
admin.GET("/email-template-versions/:id/approvals", emailTemplateHandler.GetApprovals)
admin.POST("/email-template-versions/:id/preview", emailTemplateHandler.PreviewVersion)
admin.POST("/email-template-versions/:id/send-test", emailTemplateHandler.SendTestEmail)
// =============================================
// Data Subject Requests (DSR) Management
// =============================================
admin.GET("/dsr", dsrHandler.AdminListDSR)
admin.GET("/dsr/stats", dsrHandler.AdminGetDSRStats)
admin.POST("/dsr", dsrHandler.AdminCreateDSR)
admin.GET("/dsr/:id", dsrHandler.AdminGetDSR)
admin.PUT("/dsr/:id", dsrHandler.AdminUpdateDSR)
admin.POST("/dsr/:id/status", dsrHandler.AdminUpdateDSRStatus)
admin.POST("/dsr/:id/verify-identity", dsrHandler.AdminVerifyIdentity)
admin.POST("/dsr/:id/assign", dsrHandler.AdminAssignDSR)
admin.POST("/dsr/:id/extend", dsrHandler.AdminExtendDSRDeadline)
admin.POST("/dsr/:id/complete", dsrHandler.AdminCompleteDSR)
admin.POST("/dsr/:id/reject", dsrHandler.AdminRejectDSR)
admin.GET("/dsr/:id/history", dsrHandler.AdminGetDSRHistory)
admin.GET("/dsr/:id/communications", dsrHandler.AdminGetDSRCommunications)
admin.POST("/dsr/:id/communicate", dsrHandler.AdminSendDSRCommunication)
admin.GET("/dsr/:id/exception-checks", dsrHandler.AdminGetExceptionChecks)
admin.POST("/dsr/:id/exception-checks/init", dsrHandler.AdminInitExceptionChecks)
admin.PUT("/dsr/:id/exception-checks/:checkId", dsrHandler.AdminUpdateExceptionCheck)
admin.POST("/dsr/deadlines/process", dsrHandler.ProcessDeadlines)
// DSR Templates
admin.GET("/dsr-templates", dsrHandler.AdminGetDSRTemplates)
admin.GET("/dsr-templates/published", dsrHandler.AdminGetPublishedDSRTemplates)
admin.GET("/dsr-templates/:id/versions", dsrHandler.AdminGetDSRTemplateVersions)
admin.POST("/dsr-templates/:id/versions", dsrHandler.AdminCreateDSRTemplateVersion)
admin.POST("/dsr-template-versions/:versionId/publish", dsrHandler.AdminPublishDSRTemplateVersion)
}
// =============================================
// Communication Routes (Matrix + Jitsi)
// =============================================
communicationHandler.RegisterRoutes(v1, cfg.JWTSecret, middleware.AuthMiddleware(cfg.JWTSecret))
// =============================================
// Cookie Banner SDK Routes (Public - Anonymous)
// =============================================
// Diese Endpoints werden vom @breakpilot/consent-sdk verwendet
// für anonyme (device-basierte) Cookie-Einwilligungen.
banner := v1.Group("/banner")
{
// Public Endpoints (keine Auth erforderlich)
banner.POST("/consent", h.CreateBannerConsent)
banner.GET("/consent", h.GetBannerConsent)
banner.DELETE("/consent/:consentId", h.RevokeBannerConsent)
banner.GET("/config/:siteId", h.GetSiteConfig)
banner.GET("/consent/export", h.ExportBannerConsent)
}
// Banner Admin Routes (require admin auth)
bannerAdmin := v1.Group("/banner/admin")
bannerAdmin.Use(middleware.AuthMiddleware(cfg.JWTSecret))
bannerAdmin.Use(middleware.AdminOnly())
{
bannerAdmin.GET("/stats/:siteId", h.GetBannerStats)
}
}
// Start background scheduler for scheduled publishing
go startScheduledPublishingWorker(db)
// Start DSR deadline monitoring worker
go startDSRDeadlineWorker(dsrService)
// Start server
port := cfg.Port
if port == "" {
port = "8080"
}
log.Printf("Starting Consent Service on port %s", port)
if err := router.Run(":" + port); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
// startScheduledPublishingWorker runs every minute to check for scheduled versions
func startScheduledPublishingWorker(db *database.DB) {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
log.Println("Scheduled publishing worker started (checking every minute)")
for range ticker.C {
processScheduledVersions(db)
}
}
func processScheduledVersions(db *database.DB) {
ctx := context.Background()
// Find all scheduled versions that are due
rows, err := db.Pool.Query(ctx, `
SELECT id, document_id, version
FROM document_versions
WHERE status = 'scheduled'
AND scheduled_publish_at IS NOT NULL
AND scheduled_publish_at <= NOW()
`)
if err != nil {
log.Printf("Scheduler: Error fetching scheduled versions: %v", err)
return
}
defer rows.Close()
var publishedCount int
for rows.Next() {
var versionID, docID string
var version string
if err := rows.Scan(&versionID, &docID, &version); err != nil {
continue
}
// Publish this version
_, err := db.Pool.Exec(ctx, `
UPDATE document_versions
SET status = 'published', published_at = NOW(), updated_at = NOW()
WHERE id = $1
`, versionID)
if err == nil {
// Archive previous published versions for this document
db.Pool.Exec(ctx, `
UPDATE document_versions
SET status = 'archived', updated_at = NOW()
WHERE document_id = $1 AND id != $2 AND status = 'published'
`, docID, versionID)
// Log the publishing
details := fmt.Sprintf("Version %s automatically published by scheduler", version)
db.Pool.Exec(ctx, `
INSERT INTO consent_audit_log (action, entity_type, entity_id, details, user_agent)
VALUES ('version_scheduled_published', 'document_version', $1, $2, 'scheduler')
`, versionID, details)
publishedCount++
log.Printf("Scheduler: Published version %s (ID: %s)", version, versionID)
}
}
if publishedCount > 0 {
log.Printf("Scheduler: Published %d version(s)", publishedCount)
}
}
// startDSRDeadlineWorker monitors DSR deadlines and sends notifications
func startDSRDeadlineWorker(dsrService *services.DSRService) {
ticker := time.NewTicker(1 * time.Hour)
defer ticker.Stop()
log.Println("DSR deadline monitoring worker started (checking every hour)")
// Run immediately on startup
ctx := context.Background()
if err := dsrService.ProcessDeadlines(ctx); err != nil {
log.Printf("DSR Worker: Error processing deadlines: %v", err)
}
for range ticker.C {
ctx := context.Background()
if err := dsrService.ProcessDeadlines(ctx); err != nil {
log.Printf("DSR Worker: Error processing deadlines: %v", err)
}
}
}