A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
529 lines
17 KiB
Go
529 lines
17 KiB
Go
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"})
|
|
}
|