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>
472 lines
18 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|