package handlers import ( "net/http" "strconv" "time" "github.com/breakpilot/consent-service/internal/models" "github.com/breakpilot/consent-service/internal/services" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // EmailTemplateHandler handles email template operations type EmailTemplateHandler struct { service *services.EmailTemplateService } // NewEmailTemplateHandler creates a new email template handler func NewEmailTemplateHandler(service *services.EmailTemplateService) *EmailTemplateHandler { return &EmailTemplateHandler{service: service} } // GetAllTemplateTypes returns all available email template types with their variables // GET /api/v1/admin/email-templates/types func (h *EmailTemplateHandler) GetAllTemplateTypes(c *gin.Context) { types := h.service.GetAllTemplateTypes() c.JSON(http.StatusOK, gin.H{"types": types}) } // GetAllTemplates returns all email templates with their latest published versions // GET /api/v1/admin/email-templates func (h *EmailTemplateHandler) GetAllTemplates(c *gin.Context) { templates, err := h.service.GetAllTemplates(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"templates": templates}) } // GetTemplate returns a single template by ID // GET /api/v1/admin/email-templates/:id func (h *EmailTemplateHandler) GetTemplate(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid template ID"}) return } template, err := h.service.GetTemplateByID(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "template not found"}) return } c.JSON(http.StatusOK, template) } // CreateTemplate creates a new email template type // POST /api/v1/admin/email-templates func (h *EmailTemplateHandler) CreateTemplate(c *gin.Context) { var req models.CreateEmailTemplateRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } template, err := h.service.CreateEmailTemplate(c.Request.Context(), &req) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, template) } // GetTemplateVersions returns all versions for a template // GET /api/v1/admin/email-templates/:id/versions func (h *EmailTemplateHandler) GetTemplateVersions(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid template ID"}) return } versions, err := h.service.GetVersionsByTemplateID(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"versions": versions}) } // GetVersion returns a single version by ID // GET /api/v1/admin/email-template-versions/:id func (h *EmailTemplateHandler) GetVersion(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid version ID"}) return } version, err := h.service.GetVersionByID(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "version not found"}) return } c.JSON(http.StatusOK, version) } // CreateVersion creates a new version of an email template // POST /api/v1/admin/email-template-versions func (h *EmailTemplateHandler) CreateVersion(c *gin.Context) { var req models.CreateEmailTemplateVersionRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Get user ID from context userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"}) return } uid, _ := uuid.Parse(userID.(string)) version, err := h.service.CreateTemplateVersion(c.Request.Context(), &req, uid) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusCreated, version) } // UpdateVersion updates a version // PUT /api/v1/admin/email-template-versions/:id func (h *EmailTemplateHandler) UpdateVersion(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid version ID"}) return } var req models.UpdateEmailTemplateVersionRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.service.UpdateVersion(c.Request.Context(), id, &req); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "version updated"}) } // SubmitForReview submits a version for review // POST /api/v1/admin/email-template-versions/:id/submit func (h *EmailTemplateHandler) SubmitForReview(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid version ID"}) return } var req struct { Comment *string `json:"comment"` } c.ShouldBindJSON(&req) userID, exists := c.Get("user_id") if !exists { c.JSON(http.StatusUnauthorized, gin.H{"error": "user not authenticated"}) return } uid, _ := uuid.Parse(userID.(string)) if err := h.service.SubmitForReview(c.Request.Context(), id, uid, req.Comment); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "version submitted for review"}) } // ApproveVersion approves a version (DSB only) // POST /api/v1/admin/email-template-versions/:id/approve func (h *EmailTemplateHandler) ApproveVersion(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid version ID"}) return } // Check role role, exists := c.Get("user_role") if !exists || (role != "data_protection_officer" && role != "admin" && role != "super_admin") { c.JSON(http.StatusForbidden, gin.H{"error": "insufficient permissions"}) return } var req struct { Comment *string `json:"comment"` ScheduledPublishAt *string `json:"scheduled_publish_at"` } c.ShouldBindJSON(&req) userID, _ := c.Get("user_id") uid, _ := uuid.Parse(userID.(string)) var scheduledAt *time.Time if req.ScheduledPublishAt != nil { t, err := time.Parse(time.RFC3339, *req.ScheduledPublishAt) if err == nil { scheduledAt = &t } } if err := h.service.ApproveVersion(c.Request.Context(), id, uid, req.Comment, scheduledAt); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "version approved"}) } // RejectVersion rejects a version // POST /api/v1/admin/email-template-versions/:id/reject func (h *EmailTemplateHandler) RejectVersion(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid version ID"}) return } role, exists := c.Get("user_role") if !exists || (role != "data_protection_officer" && role != "admin" && role != "super_admin") { c.JSON(http.StatusForbidden, gin.H{"error": "insufficient permissions"}) return } var req struct { Comment string `json:"comment" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "comment is required"}) return } userID, _ := c.Get("user_id") uid, _ := uuid.Parse(userID.(string)) if err := h.service.RejectVersion(c.Request.Context(), id, uid, req.Comment); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "version rejected"}) } // PublishVersion publishes an approved version // POST /api/v1/admin/email-template-versions/:id/publish func (h *EmailTemplateHandler) PublishVersion(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid version ID"}) return } role, exists := c.Get("user_role") if !exists || (role != "data_protection_officer" && role != "admin" && role != "super_admin") { c.JSON(http.StatusForbidden, gin.H{"error": "insufficient permissions"}) return } userID, _ := c.Get("user_id") uid, _ := uuid.Parse(userID.(string)) if err := h.service.PublishVersion(c.Request.Context(), id, uid); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "version published"}) } // GetApprovals returns approval history for a version // GET /api/v1/admin/email-template-versions/:id/approvals func (h *EmailTemplateHandler) GetApprovals(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid version ID"}) return } approvals, err := h.service.GetApprovals(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"approvals": approvals}) } // PreviewVersion renders a preview of an email template version // POST /api/v1/admin/email-template-versions/:id/preview func (h *EmailTemplateHandler) PreviewVersion(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid version ID"}) return } var req struct { Variables map[string]string `json:"variables"` } c.ShouldBindJSON(&req) version, err := h.service.GetVersionByID(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "version not found"}) return } // Use default test values if not provided if req.Variables == nil { req.Variables = map[string]string{ "user_name": "Max Mustermann", "user_email": "max@example.com", "login_url": "https://breakpilot.app/login", "support_email": "support@breakpilot.app", "verification_url": "https://breakpilot.app/verify?token=abc123", "verification_code": "123456", "expires_in": "24 Stunden", "reset_url": "https://breakpilot.app/reset?token=xyz789", "reset_code": "RESET123", "ip_address": "192.168.1.1", "device_info": "Chrome auf Windows 11", "changed_at": time.Now().Format("02.01.2006 15:04"), "enabled_at": time.Now().Format("02.01.2006 15:04"), "disabled_at": time.Now().Format("02.01.2006 15:04"), "support_url": "https://breakpilot.app/support", "security_url": "https://breakpilot.app/account/security", "login_time": time.Now().Format("02.01.2006 15:04"), "location": "Berlin, Deutschland", "activity_type": "Mehrere fehlgeschlagene Login-Versuche", "activity_time": time.Now().Format("02.01.2006 15:04"), "locked_at": time.Now().Format("02.01.2006 15:04"), "reason": "Zu viele fehlgeschlagene Login-Versuche", "unlock_time": time.Now().Add(30 * time.Minute).Format("02.01.2006 15:04"), "unlocked_at": time.Now().Format("02.01.2006 15:04"), "requested_at": time.Now().Format("02.01.2006"), "deletion_date": time.Now().AddDate(0, 0, 30).Format("02.01.2006"), "cancel_url": "https://breakpilot.app/cancel-deletion?token=cancel123", "data_info": "Benutzerdaten, Zustimmungshistorie, Audit-Logs", "deleted_at": time.Now().Format("02.01.2006"), "feedback_url": "https://breakpilot.app/feedback", "download_url": "https://breakpilot.app/export/download?token=export123", "file_size": "2.3 MB", "old_email": "alt@example.com", "new_email": "neu@example.com", "document_name": "Datenschutzerklärung", "document_type": "privacy", "version": "2.0.0", "consent_url": "https://breakpilot.app/consent", "deadline": time.Now().AddDate(0, 0, 14).Format("02.01.2006"), "days_left": "7", "hours_left": "24 Stunden", "consequences": "Ohne Ihre Zustimmung wird Ihr Konto suspendiert.", "suspended_at": time.Now().Format("02.01.2006 15:04"), "documents": "- Datenschutzerklärung v2.0.0\n- AGB v1.5.0", } } preview, err := h.service.RenderTemplate(version, req.Variables) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, preview) } // SendTestEmail sends a test email // POST /api/v1/admin/email-template-versions/:id/send-test func (h *EmailTemplateHandler) SendTestEmail(c *gin.Context) { idStr := c.Param("id") id, err := uuid.Parse(idStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid version ID"}) return } var req models.SendTestEmailRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } req.VersionID = idStr version, err := h.service.GetVersionByID(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "version not found"}) return } // Get template to find type template, err := h.service.GetTemplateByID(c.Request.Context(), version.TemplateID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "template not found"}) return } userID, _ := c.Get("user_id") uid, _ := uuid.Parse(userID.(string)) // Send test email if err := h.service.SendEmail(c.Request.Context(), template.Type, version.Language, req.Recipient, req.Variables, &uid); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "test email sent"}) } // GetSettings returns global email settings // GET /api/v1/admin/email-templates/settings func (h *EmailTemplateHandler) GetSettings(c *gin.Context) { settings, err := h.service.GetSettings(c.Request.Context()) if err != nil { // Return default settings if none exist c.JSON(http.StatusOK, gin.H{ "company_name": "BreakPilot", "sender_name": "BreakPilot", "sender_email": "noreply@breakpilot.app", "primary_color": "#2563eb", "secondary_color": "#64748b", }) return } c.JSON(http.StatusOK, settings) } // UpdateSettings updates global email settings // PUT /api/v1/admin/email-templates/settings func (h *EmailTemplateHandler) UpdateSettings(c *gin.Context) { var req models.UpdateEmailTemplateSettingsRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } userID, _ := c.Get("user_id") uid, _ := uuid.Parse(userID.(string)) if err := h.service.UpdateSettings(c.Request.Context(), &req, uid); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "settings updated"}) } // GetEmailStats returns email statistics // GET /api/v1/admin/email-templates/stats func (h *EmailTemplateHandler) GetEmailStats(c *gin.Context) { stats, err := h.service.GetEmailStats(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, stats) } // GetSendLogs returns email send logs // GET /api/v1/admin/email-templates/logs func (h *EmailTemplateHandler) GetSendLogs(c *gin.Context) { limitStr := c.DefaultQuery("limit", "50") offsetStr := c.DefaultQuery("offset", "0") limit, _ := strconv.Atoi(limitStr) offset, _ := strconv.Atoi(offsetStr) if limit > 100 { limit = 100 } logs, total, err := h.service.GetSendLogs(c.Request.Context(), limit, offset) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"logs": logs, "total": total}) } // GetDefaultContent returns default template content for a type // GET /api/v1/admin/email-templates/default/:type func (h *EmailTemplateHandler) GetDefaultContent(c *gin.Context) { templateType := c.Param("type") language := c.DefaultQuery("language", "de") subject, bodyHTML, bodyText := h.service.GetDefaultTemplateContent(templateType, language) c.JSON(http.StatusOK, gin.H{ "subject": subject, "body_html": bodyHTML, "body_text": bodyText, }) } // InitializeTemplates initializes default email templates // POST /api/v1/admin/email-templates/initialize func (h *EmailTemplateHandler) InitializeTemplates(c *gin.Context) { role, exists := c.Get("user_role") if !exists || (role != "admin" && role != "super_admin") { c.JSON(http.StatusForbidden, gin.H{"error": "insufficient permissions"}) return } if err := h.service.InitDefaultTemplates(c.Request.Context()); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "default templates initialized"}) }