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) } } }