This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/consent-service/internal/services/email_template_service.go
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
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>
2026-02-09 09:51:32 +01:00

1674 lines
62 KiB
Go

package services
import (
"context"
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
"github.com/breakpilot/consent-service/internal/models"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
// EmailTemplateService handles email template management
type EmailTemplateService struct {
db *pgxpool.Pool
}
// NewEmailTemplateService creates a new email template service
func NewEmailTemplateService(db *pgxpool.Pool) *EmailTemplateService {
return &EmailTemplateService{db: db}
}
// GetAllTemplateTypes returns all available email template types with their variables
func (s *EmailTemplateService) GetAllTemplateTypes() []models.EmailTemplateVariables {
return []models.EmailTemplateVariables{
{
TemplateType: models.EmailTypeWelcome,
Variables: []string{"user_name", "user_email", "login_url", "support_email"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"user_email": "E-Mail-Adresse des Benutzers",
"login_url": "URL zur Login-Seite",
"support_email": "Support E-Mail-Adresse",
},
},
{
TemplateType: models.EmailTypeEmailVerification,
Variables: []string{"user_name", "verification_url", "verification_code", "expires_in"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"verification_url": "URL zur E-Mail-Verifizierung",
"verification_code": "Verifizierungscode",
"expires_in": "Gültigkeit des Links (z.B. '24 Stunden')",
},
},
{
TemplateType: models.EmailTypePasswordReset,
Variables: []string{"user_name", "reset_url", "reset_code", "expires_in", "ip_address"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"reset_url": "URL zum Passwort-Reset",
"reset_code": "Reset-Code",
"expires_in": "Gültigkeit des Links",
"ip_address": "IP-Adresse der Anfrage",
},
},
{
TemplateType: models.EmailTypePasswordChanged,
Variables: []string{"user_name", "changed_at", "ip_address", "device_info", "support_url"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"changed_at": "Zeitpunkt der Änderung",
"ip_address": "IP-Adresse",
"device_info": "Geräte-Informationen",
"support_url": "URL zum Support",
},
},
{
TemplateType: models.EmailType2FAEnabled,
Variables: []string{"user_name", "enabled_at", "device_info", "security_url"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"enabled_at": "Zeitpunkt der Aktivierung",
"device_info": "Geräte-Informationen",
"security_url": "URL zu Sicherheitseinstellungen",
},
},
{
TemplateType: models.EmailType2FADisabled,
Variables: []string{"user_name", "disabled_at", "ip_address", "security_url"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"disabled_at": "Zeitpunkt der Deaktivierung",
"ip_address": "IP-Adresse",
"security_url": "URL zu Sicherheitseinstellungen",
},
},
{
TemplateType: models.EmailTypeNewDeviceLogin,
Variables: []string{"user_name", "login_time", "ip_address", "device_info", "location", "security_url"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"login_time": "Zeitpunkt des Logins",
"ip_address": "IP-Adresse",
"device_info": "Geräte-Informationen",
"location": "Ungefährer Standort",
"security_url": "URL zu Sicherheitseinstellungen",
},
},
{
TemplateType: models.EmailTypeSuspiciousActivity,
Variables: []string{"user_name", "activity_type", "activity_time", "ip_address", "security_url"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"activity_type": "Art der Aktivität",
"activity_time": "Zeitpunkt",
"ip_address": "IP-Adresse",
"security_url": "URL zu Sicherheitseinstellungen",
},
},
{
TemplateType: models.EmailTypeAccountLocked,
Variables: []string{"user_name", "locked_at", "reason", "unlock_time", "support_url"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"locked_at": "Zeitpunkt der Sperrung",
"reason": "Grund der Sperrung",
"unlock_time": "Zeitpunkt der automatischen Entsperrung",
"support_url": "URL zum Support",
},
},
{
TemplateType: models.EmailTypeAccountUnlocked,
Variables: []string{"user_name", "unlocked_at", "login_url"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"unlocked_at": "Zeitpunkt der Entsperrung",
"login_url": "URL zur Login-Seite",
},
},
{
TemplateType: models.EmailTypeDeletionRequested,
Variables: []string{"user_name", "requested_at", "deletion_date", "cancel_url", "data_info"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"requested_at": "Zeitpunkt der Anfrage",
"deletion_date": "Datum der endgültigen Löschung",
"cancel_url": "URL zum Abbrechen",
"data_info": "Info über zu löschende Daten",
},
},
{
TemplateType: models.EmailTypeDeletionConfirmed,
Variables: []string{"user_name", "deleted_at", "feedback_url"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"deleted_at": "Zeitpunkt der Löschung",
"feedback_url": "URL für Feedback",
},
},
{
TemplateType: models.EmailTypeDataExportReady,
Variables: []string{"user_name", "download_url", "expires_in", "file_size"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"download_url": "URL zum Download",
"expires_in": "Gültigkeit des Download-Links",
"file_size": "Dateigröße",
},
},
{
TemplateType: models.EmailTypeEmailChanged,
Variables: []string{"user_name", "old_email", "new_email", "changed_at", "support_url"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"old_email": "Alte E-Mail-Adresse",
"new_email": "Neue E-Mail-Adresse",
"changed_at": "Zeitpunkt der Änderung",
"support_url": "URL zum Support",
},
},
{
TemplateType: models.EmailTypeEmailChangeVerify,
Variables: []string{"user_name", "new_email", "verification_url", "expires_in"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"new_email": "Neue E-Mail-Adresse",
"verification_url": "URL zur Verifizierung",
"expires_in": "Gültigkeit des Links",
},
},
{
TemplateType: models.EmailTypeNewVersionPublished,
Variables: []string{"user_name", "document_name", "document_type", "version", "consent_url", "deadline"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"document_name": "Name des Dokuments",
"document_type": "Typ des Dokuments",
"version": "Versionsnummer",
"consent_url": "URL zur Zustimmung",
"deadline": "Frist für die Zustimmung",
},
},
{
TemplateType: models.EmailTypeConsentReminder,
Variables: []string{"user_name", "document_name", "days_left", "consent_url", "deadline"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"document_name": "Name des Dokuments",
"days_left": "Verbleibende Tage",
"consent_url": "URL zur Zustimmung",
"deadline": "Frist für die Zustimmung",
},
},
{
TemplateType: models.EmailTypeConsentDeadlineWarning,
Variables: []string{"user_name", "document_name", "hours_left", "consent_url", "consequences"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"document_name": "Name des Dokuments",
"hours_left": "Verbleibende Stunden",
"consent_url": "URL zur Zustimmung",
"consequences": "Konsequenzen bei Nicht-Zustimmung",
},
},
{
TemplateType: models.EmailTypeAccountSuspended,
Variables: []string{"user_name", "suspended_at", "reason", "documents", "consent_url"},
Descriptions: map[string]string{
"user_name": "Name des Benutzers",
"suspended_at": "Zeitpunkt der Suspendierung",
"reason": "Grund der Suspendierung",
"documents": "Liste der fehlenden Zustimmungen",
"consent_url": "URL zur Zustimmung",
},
},
}
}
// CreateEmailTemplate creates a new email template type
func (s *EmailTemplateService) CreateEmailTemplate(ctx context.Context, req *models.CreateEmailTemplateRequest) (*models.EmailTemplate, error) {
template := &models.EmailTemplate{
ID: uuid.New(),
Type: req.Type,
Name: req.Name,
Description: req.Description,
IsActive: true,
SortOrder: 0,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
_, err := s.db.Exec(ctx, `
INSERT INTO email_templates (id, type, name, description, is_active, sort_order, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`, template.ID, template.Type, template.Name, template.Description, template.IsActive, template.SortOrder, template.CreatedAt, template.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to create email template: %w", err)
}
return template, nil
}
// GetAllTemplates returns all email templates with their latest published versions
func (s *EmailTemplateService) GetAllTemplates(ctx context.Context) ([]models.EmailTemplateWithVersion, error) {
rows, err := s.db.Query(ctx, `
SELECT
t.id, t.type, t.name, t.description, t.is_active, t.sort_order, t.created_at, t.updated_at,
v.id, v.template_id, v.version, v.language, v.subject, v.body_html, v.body_text,
v.summary, v.status, v.published_at, v.scheduled_publish_at, v.created_by,
v.approved_by, v.approved_at, v.created_at, v.updated_at
FROM email_templates t
LEFT JOIN email_template_versions v ON t.id = v.template_id AND v.status = 'published'
ORDER BY t.sort_order, t.name
`)
if err != nil {
return nil, fmt.Errorf("failed to get email templates: %w", err)
}
defer rows.Close()
var results []models.EmailTemplateWithVersion
for rows.Next() {
var template models.EmailTemplate
var versionID, templateID, createdBy, approvedBy *uuid.UUID
var publishedAt, scheduledPublishAt, approvedAt, vCreatedAt, vUpdatedAt *time.Time
var vVersion, vLanguage, vSubject, vBodyHTML, vBodyText, vSummary, vStatus *string
err := rows.Scan(
&template.ID, &template.Type, &template.Name, &template.Description,
&template.IsActive, &template.SortOrder, &template.CreatedAt, &template.UpdatedAt,
&versionID, &templateID, &vVersion, &vLanguage, &vSubject, &vBodyHTML, &vBodyText,
&vSummary, &vStatus, &publishedAt, &scheduledPublishAt, &createdBy,
&approvedBy, &approvedAt, &vCreatedAt, &vUpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan email template: %w", err)
}
result := models.EmailTemplateWithVersion{Template: template}
if versionID != nil {
result.LatestVersion = &models.EmailTemplateVersion{
ID: *versionID,
TemplateID: *templateID,
Version: *vVersion,
Language: *vLanguage,
Subject: *vSubject,
BodyHTML: *vBodyHTML,
BodyText: *vBodyText,
Status: *vStatus,
PublishedAt: publishedAt,
ScheduledPublishAt: scheduledPublishAt,
CreatedBy: createdBy,
ApprovedBy: approvedBy,
ApprovedAt: approvedAt,
}
if vSummary != nil {
result.LatestVersion.Summary = vSummary
}
if vCreatedAt != nil {
result.LatestVersion.CreatedAt = *vCreatedAt
}
if vUpdatedAt != nil {
result.LatestVersion.UpdatedAt = *vUpdatedAt
}
}
results = append(results, result)
}
return results, nil
}
// GetTemplateByID returns a template by ID
func (s *EmailTemplateService) GetTemplateByID(ctx context.Context, id uuid.UUID) (*models.EmailTemplate, error) {
var template models.EmailTemplate
err := s.db.QueryRow(ctx, `
SELECT id, type, name, description, is_active, sort_order, created_at, updated_at
FROM email_templates WHERE id = $1
`, id).Scan(&template.ID, &template.Type, &template.Name, &template.Description,
&template.IsActive, &template.SortOrder, &template.CreatedAt, &template.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to get email template: %w", err)
}
return &template, nil
}
// GetTemplateByType returns a template by type
func (s *EmailTemplateService) GetTemplateByType(ctx context.Context, templateType string) (*models.EmailTemplate, error) {
var template models.EmailTemplate
err := s.db.QueryRow(ctx, `
SELECT id, type, name, description, is_active, sort_order, created_at, updated_at
FROM email_templates WHERE type = $1
`, templateType).Scan(&template.ID, &template.Type, &template.Name, &template.Description,
&template.IsActive, &template.SortOrder, &template.CreatedAt, &template.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to get email template: %w", err)
}
return &template, nil
}
// CreateTemplateVersion creates a new version of an email template
func (s *EmailTemplateService) CreateTemplateVersion(ctx context.Context, req *models.CreateEmailTemplateVersionRequest, createdBy uuid.UUID) (*models.EmailTemplateVersion, error) {
templateID, err := uuid.Parse(req.TemplateID)
if err != nil {
return nil, fmt.Errorf("invalid template ID: %w", err)
}
version := &models.EmailTemplateVersion{
ID: uuid.New(),
TemplateID: templateID,
Version: req.Version,
Language: req.Language,
Subject: req.Subject,
BodyHTML: req.BodyHTML,
BodyText: req.BodyText,
Summary: req.Summary,
Status: "draft",
CreatedBy: &createdBy,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
_, err = s.db.Exec(ctx, `
INSERT INTO email_template_versions
(id, template_id, version, language, subject, body_html, body_text, summary, status, created_by, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
`, version.ID, version.TemplateID, version.Version, version.Language, version.Subject,
version.BodyHTML, version.BodyText, version.Summary, version.Status, version.CreatedBy,
version.CreatedAt, version.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to create email template version: %w", err)
}
return version, nil
}
// GetVersionsByTemplateID returns all versions for a template
func (s *EmailTemplateService) GetVersionsByTemplateID(ctx context.Context, templateID uuid.UUID) ([]models.EmailTemplateVersion, error) {
rows, err := s.db.Query(ctx, `
SELECT id, template_id, version, language, subject, body_html, body_text, summary, status,
published_at, scheduled_publish_at, created_by, approved_by, approved_at, created_at, updated_at
FROM email_template_versions
WHERE template_id = $1
ORDER BY created_at DESC
`, templateID)
if err != nil {
return nil, fmt.Errorf("failed to get template versions: %w", err)
}
defer rows.Close()
var versions []models.EmailTemplateVersion
for rows.Next() {
var v models.EmailTemplateVersion
err := rows.Scan(&v.ID, &v.TemplateID, &v.Version, &v.Language, &v.Subject, &v.BodyHTML, &v.BodyText,
&v.Summary, &v.Status, &v.PublishedAt, &v.ScheduledPublishAt, &v.CreatedBy, &v.ApprovedBy,
&v.ApprovedAt, &v.CreatedAt, &v.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to scan template version: %w", err)
}
versions = append(versions, v)
}
return versions, nil
}
// GetVersionByID returns a version by ID
func (s *EmailTemplateService) GetVersionByID(ctx context.Context, id uuid.UUID) (*models.EmailTemplateVersion, error) {
var v models.EmailTemplateVersion
err := s.db.QueryRow(ctx, `
SELECT id, template_id, version, language, subject, body_html, body_text, summary, status,
published_at, scheduled_publish_at, created_by, approved_by, approved_at, created_at, updated_at
FROM email_template_versions WHERE id = $1
`, id).Scan(&v.ID, &v.TemplateID, &v.Version, &v.Language, &v.Subject, &v.BodyHTML, &v.BodyText,
&v.Summary, &v.Status, &v.PublishedAt, &v.ScheduledPublishAt, &v.CreatedBy, &v.ApprovedBy,
&v.ApprovedAt, &v.CreatedAt, &v.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to get template version: %w", err)
}
return &v, nil
}
// GetPublishedVersion returns the published version for a template and language
func (s *EmailTemplateService) GetPublishedVersion(ctx context.Context, templateType, language string) (*models.EmailTemplateVersion, error) {
var v models.EmailTemplateVersion
err := s.db.QueryRow(ctx, `
SELECT v.id, v.template_id, v.version, v.language, v.subject, v.body_html, v.body_text,
v.summary, v.status, v.published_at, v.scheduled_publish_at, v.created_by,
v.approved_by, v.approved_at, v.created_at, v.updated_at
FROM email_template_versions v
JOIN email_templates t ON t.id = v.template_id
WHERE t.type = $1 AND v.language = $2 AND v.status = 'published'
ORDER BY v.published_at DESC
LIMIT 1
`, templateType, language).Scan(&v.ID, &v.TemplateID, &v.Version, &v.Language, &v.Subject, &v.BodyHTML, &v.BodyText,
&v.Summary, &v.Status, &v.PublishedAt, &v.ScheduledPublishAt, &v.CreatedBy, &v.ApprovedBy,
&v.ApprovedAt, &v.CreatedAt, &v.UpdatedAt)
if err != nil {
return nil, fmt.Errorf("failed to get published version: %w", err)
}
return &v, nil
}
// UpdateVersion updates a version
func (s *EmailTemplateService) UpdateVersion(ctx context.Context, id uuid.UUID, req *models.UpdateEmailTemplateVersionRequest) error {
query := "UPDATE email_template_versions SET updated_at = $1"
args := []interface{}{time.Now()}
argIdx := 2
if req.Subject != nil {
query += fmt.Sprintf(", subject = $%d", argIdx)
args = append(args, *req.Subject)
argIdx++
}
if req.BodyHTML != nil {
query += fmt.Sprintf(", body_html = $%d", argIdx)
args = append(args, *req.BodyHTML)
argIdx++
}
if req.BodyText != nil {
query += fmt.Sprintf(", body_text = $%d", argIdx)
args = append(args, *req.BodyText)
argIdx++
}
if req.Summary != nil {
query += fmt.Sprintf(", summary = $%d", argIdx)
args = append(args, *req.Summary)
argIdx++
}
if req.Status != nil {
query += fmt.Sprintf(", status = $%d", argIdx)
args = append(args, *req.Status)
argIdx++
}
query += fmt.Sprintf(" WHERE id = $%d", argIdx)
args = append(args, id)
_, err := s.db.Exec(ctx, query, args...)
if err != nil {
return fmt.Errorf("failed to update version: %w", err)
}
return nil
}
// SubmitForReview submits a version for review
func (s *EmailTemplateService) SubmitForReview(ctx context.Context, versionID, submitterID uuid.UUID, comment *string) error {
tx, err := s.db.Begin(ctx)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
// Update status
_, err = tx.Exec(ctx, `
UPDATE email_template_versions SET status = 'review', updated_at = $1 WHERE id = $2
`, time.Now(), versionID)
if err != nil {
return fmt.Errorf("failed to update status: %w", err)
}
// Create approval record
_, err = tx.Exec(ctx, `
INSERT INTO email_template_approvals (id, version_id, approver_id, action, comment, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
`, uuid.New(), versionID, submitterID, "submitted_for_review", comment, time.Now())
if err != nil {
return fmt.Errorf("failed to create approval record: %w", err)
}
return tx.Commit(ctx)
}
// ApproveVersion approves a version (DSB)
func (s *EmailTemplateService) ApproveVersion(ctx context.Context, versionID, approverID uuid.UUID, comment *string, scheduledPublishAt *time.Time) error {
tx, err := s.db.Begin(ctx)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
now := time.Now()
status := "approved"
if scheduledPublishAt != nil {
status = "scheduled"
}
_, err = tx.Exec(ctx, `
UPDATE email_template_versions
SET status = $1, approved_by = $2, approved_at = $3, scheduled_publish_at = $4, updated_at = $5
WHERE id = $6
`, status, approverID, now, scheduledPublishAt, now, versionID)
if err != nil {
return fmt.Errorf("failed to approve version: %w", err)
}
_, err = tx.Exec(ctx, `
INSERT INTO email_template_approvals (id, version_id, approver_id, action, comment, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
`, uuid.New(), versionID, approverID, "approved", comment, now)
if err != nil {
return fmt.Errorf("failed to create approval record: %w", err)
}
return tx.Commit(ctx)
}
// PublishVersion publishes an approved version
func (s *EmailTemplateService) PublishVersion(ctx context.Context, versionID, publisherID uuid.UUID) error {
tx, err := s.db.Begin(ctx)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
// Get version info to find template and language
var templateID uuid.UUID
var language string
err = tx.QueryRow(ctx, `SELECT template_id, language FROM email_template_versions WHERE id = $1`, versionID).Scan(&templateID, &language)
if err != nil {
return fmt.Errorf("failed to get version info: %w", err)
}
// Archive old published versions for same template and language
_, err = tx.Exec(ctx, `
UPDATE email_template_versions
SET status = 'archived', updated_at = $1
WHERE template_id = $2 AND language = $3 AND status = 'published'
`, time.Now(), templateID, language)
if err != nil {
return fmt.Errorf("failed to archive old versions: %w", err)
}
// Publish the new version
now := time.Now()
_, err = tx.Exec(ctx, `
UPDATE email_template_versions
SET status = 'published', published_at = $1, updated_at = $2
WHERE id = $3
`, now, now, versionID)
if err != nil {
return fmt.Errorf("failed to publish version: %w", err)
}
// Create approval record
_, err = tx.Exec(ctx, `
INSERT INTO email_template_approvals (id, version_id, approver_id, action, created_at)
VALUES ($1, $2, $3, $4, $5)
`, uuid.New(), versionID, publisherID, "published", now)
if err != nil {
return fmt.Errorf("failed to create approval record: %w", err)
}
return tx.Commit(ctx)
}
// RejectVersion rejects a version
func (s *EmailTemplateService) RejectVersion(ctx context.Context, versionID, rejectorID uuid.UUID, comment string) error {
tx, err := s.db.Begin(ctx)
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
defer tx.Rollback(ctx)
now := time.Now()
_, err = tx.Exec(ctx, `
UPDATE email_template_versions SET status = 'draft', updated_at = $1 WHERE id = $2
`, now, versionID)
if err != nil {
return fmt.Errorf("failed to reject version: %w", err)
}
_, err = tx.Exec(ctx, `
INSERT INTO email_template_approvals (id, version_id, approver_id, action, comment, created_at)
VALUES ($1, $2, $3, $4, $5, $6)
`, uuid.New(), versionID, rejectorID, "rejected", &comment, now)
if err != nil {
return fmt.Errorf("failed to create approval record: %w", err)
}
return tx.Commit(ctx)
}
// GetApprovals returns approval history for a version
func (s *EmailTemplateService) GetApprovals(ctx context.Context, versionID uuid.UUID) ([]models.EmailTemplateApproval, error) {
rows, err := s.db.Query(ctx, `
SELECT id, version_id, approver_id, action, comment, created_at
FROM email_template_approvals
WHERE version_id = $1
ORDER BY created_at DESC
`, versionID)
if err != nil {
return nil, fmt.Errorf("failed to get approvals: %w", err)
}
defer rows.Close()
var approvals []models.EmailTemplateApproval
for rows.Next() {
var a models.EmailTemplateApproval
err := rows.Scan(&a.ID, &a.VersionID, &a.ApproverID, &a.Action, &a.Comment, &a.CreatedAt)
if err != nil {
return nil, fmt.Errorf("failed to scan approval: %w", err)
}
approvals = append(approvals, a)
}
return approvals, nil
}
// GetSettings returns global email settings
func (s *EmailTemplateService) GetSettings(ctx context.Context) (*models.EmailTemplateSettings, error) {
var settings models.EmailTemplateSettings
err := s.db.QueryRow(ctx, `
SELECT id, logo_url, logo_base64, company_name, sender_name, sender_email,
reply_to_email, footer_html, footer_text, primary_color, secondary_color,
updated_at, updated_by
FROM email_template_settings
LIMIT 1
`).Scan(&settings.ID, &settings.LogoURL, &settings.LogoBase64, &settings.CompanyName,
&settings.SenderName, &settings.SenderEmail, &settings.ReplyToEmail, &settings.FooterHTML,
&settings.FooterText, &settings.PrimaryColor, &settings.SecondaryColor,
&settings.UpdatedAt, &settings.UpdatedBy)
if err != nil {
return nil, fmt.Errorf("failed to get email settings: %w", err)
}
return &settings, nil
}
// UpdateSettings updates global email settings
func (s *EmailTemplateService) UpdateSettings(ctx context.Context, req *models.UpdateEmailTemplateSettingsRequest, updatedBy uuid.UUID) error {
query := "UPDATE email_template_settings SET updated_at = $1, updated_by = $2"
args := []interface{}{time.Now(), updatedBy}
argIdx := 3
if req.LogoURL != nil {
query += fmt.Sprintf(", logo_url = $%d", argIdx)
args = append(args, *req.LogoURL)
argIdx++
}
if req.LogoBase64 != nil {
query += fmt.Sprintf(", logo_base64 = $%d", argIdx)
args = append(args, *req.LogoBase64)
argIdx++
}
if req.CompanyName != nil {
query += fmt.Sprintf(", company_name = $%d", argIdx)
args = append(args, *req.CompanyName)
argIdx++
}
if req.SenderName != nil {
query += fmt.Sprintf(", sender_name = $%d", argIdx)
args = append(args, *req.SenderName)
argIdx++
}
if req.SenderEmail != nil {
query += fmt.Sprintf(", sender_email = $%d", argIdx)
args = append(args, *req.SenderEmail)
argIdx++
}
if req.ReplyToEmail != nil {
query += fmt.Sprintf(", reply_to_email = $%d", argIdx)
args = append(args, *req.ReplyToEmail)
argIdx++
}
if req.FooterHTML != nil {
query += fmt.Sprintf(", footer_html = $%d", argIdx)
args = append(args, *req.FooterHTML)
argIdx++
}
if req.FooterText != nil {
query += fmt.Sprintf(", footer_text = $%d", argIdx)
args = append(args, *req.FooterText)
argIdx++
}
if req.PrimaryColor != nil {
query += fmt.Sprintf(", primary_color = $%d", argIdx)
args = append(args, *req.PrimaryColor)
argIdx++
}
if req.SecondaryColor != nil {
query += fmt.Sprintf(", secondary_color = $%d", argIdx)
args = append(args, *req.SecondaryColor)
argIdx++
}
_, err := s.db.Exec(ctx, query, args...)
if err != nil {
return fmt.Errorf("failed to update settings: %w", err)
}
return nil
}
// RenderTemplate renders a template with variables
func (s *EmailTemplateService) RenderTemplate(version *models.EmailTemplateVersion, variables map[string]string) (*models.EmailPreviewResponse, error) {
subject := version.Subject
bodyHTML := version.BodyHTML
bodyText := version.BodyText
// Replace variables in format {{variable_name}}
re := regexp.MustCompile(`\{\{(\w+)\}\}`)
replaceFunc := func(content string) string {
return re.ReplaceAllStringFunc(content, func(match string) string {
varName := strings.Trim(match, "{}")
if val, ok := variables[varName]; ok {
return val
}
return match // Keep placeholder if variable not provided
})
}
return &models.EmailPreviewResponse{
Subject: replaceFunc(subject),
BodyHTML: replaceFunc(bodyHTML),
BodyText: replaceFunc(bodyText),
}, nil
}
// LogEmailSend logs a sent email
func (s *EmailTemplateService) LogEmailSend(ctx context.Context, log *models.EmailSendLog) error {
_, err := s.db.Exec(ctx, `
INSERT INTO email_send_logs
(id, user_id, version_id, recipient, subject, status, error_msg, variables, sent_at, created_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
`, log.ID, log.UserID, log.VersionID, log.Recipient, log.Subject, log.Status,
log.ErrorMsg, log.Variables, log.SentAt, log.CreatedAt)
if err != nil {
return fmt.Errorf("failed to log email send: %w", err)
}
return nil
}
// GetEmailStats returns email statistics
func (s *EmailTemplateService) GetEmailStats(ctx context.Context) (*models.EmailStats, error) {
var stats models.EmailStats
err := s.db.QueryRow(ctx, `
SELECT
COUNT(*) as total_sent,
COUNT(*) FILTER (WHERE status = 'delivered') as delivered,
COUNT(*) FILTER (WHERE status = 'bounced') as bounced,
COUNT(*) FILTER (WHERE status = 'failed') as failed,
COUNT(*) FILTER (WHERE created_at > NOW() - INTERVAL '7 days') as recent_sent
FROM email_send_logs
`).Scan(&stats.TotalSent, &stats.Delivered, &stats.Bounced, &stats.Failed, &stats.RecentSent)
if err != nil {
return nil, fmt.Errorf("failed to get email stats: %w", err)
}
if stats.TotalSent > 0 {
stats.DeliveryRate = float64(stats.Delivered) / float64(stats.TotalSent) * 100
}
return &stats, nil
}
// GetDefaultTemplateContent returns default content for a template type
func (s *EmailTemplateService) GetDefaultTemplateContent(templateType, language string) (subject, bodyHTML, bodyText string) {
// Default templates in German
if language == "de" {
switch templateType {
case models.EmailTypeWelcome:
return s.getWelcomeTemplateDE()
case models.EmailTypeEmailVerification:
return s.getEmailVerificationTemplateDE()
case models.EmailTypePasswordReset:
return s.getPasswordResetTemplateDE()
case models.EmailTypePasswordChanged:
return s.getPasswordChangedTemplateDE()
case models.EmailType2FAEnabled:
return s.get2FAEnabledTemplateDE()
case models.EmailType2FADisabled:
return s.get2FADisabledTemplateDE()
case models.EmailTypeNewDeviceLogin:
return s.getNewDeviceLoginTemplateDE()
case models.EmailTypeSuspiciousActivity:
return s.getSuspiciousActivityTemplateDE()
case models.EmailTypeAccountLocked:
return s.getAccountLockedTemplateDE()
case models.EmailTypeAccountUnlocked:
return s.getAccountUnlockedTemplateDE()
case models.EmailTypeDeletionRequested:
return s.getDeletionRequestedTemplateDE()
case models.EmailTypeDeletionConfirmed:
return s.getDeletionConfirmedTemplateDE()
case models.EmailTypeDataExportReady:
return s.getDataExportReadyTemplateDE()
case models.EmailTypeEmailChanged:
return s.getEmailChangedTemplateDE()
case models.EmailTypeNewVersionPublished:
return s.getNewVersionPublishedTemplateDE()
case models.EmailTypeConsentReminder:
return s.getConsentReminderTemplateDE()
case models.EmailTypeConsentDeadlineWarning:
return s.getConsentDeadlineWarningTemplateDE()
case models.EmailTypeAccountSuspended:
return s.getAccountSuspendedTemplateDE()
}
}
// Default English fallback
return "No template", "<p>No template available</p>", "No template available"
}
// ========================================
// Default German Templates
// ========================================
func (s *EmailTemplateService) getWelcomeTemplateDE() (string, string, string) {
subject := "Willkommen bei BreakPilot!"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #2563eb;">Willkommen bei BreakPilot!</h1>
<p>Hallo {{user_name}},</p>
<p>vielen Dank für Ihre Registrierung bei BreakPilot. Ihr Konto wurde erfolgreich erstellt.</p>
<p>Sie können sich jetzt mit Ihrer E-Mail-Adresse <strong>{{user_email}}</strong> anmelden:</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{login_url}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px;">Jetzt anmelden</a>
</p>
<p>Bei Fragen stehen wir Ihnen gerne zur Verfügung unter {{support_email}}.</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Willkommen bei BreakPilot!
Hallo {{user_name}},
vielen Dank für Ihre Registrierung bei BreakPilot. Ihr Konto wurde erfolgreich erstellt.
Sie können sich jetzt mit Ihrer E-Mail-Adresse {{user_email}} anmelden:
{{login_url}}
Bei Fragen stehen wir Ihnen gerne zur Verfügung unter {{support_email}}.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getEmailVerificationTemplateDE() (string, string, string) {
subject := "Bitte bestätigen Sie Ihre E-Mail-Adresse"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #2563eb;">E-Mail-Adresse bestätigen</h1>
<p>Hallo {{user_name}},</p>
<p>bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf den folgenden Link klicken:</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{verification_url}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px;">E-Mail bestätigen</a>
</p>
<p>Alternativ können Sie auch diesen Bestätigungscode eingeben: <strong>{{verification_code}}</strong></p>
<p><strong>Hinweis:</strong> Dieser Link ist nur {{expires_in}} gültig.</p>
<p>Falls Sie diese E-Mail nicht angefordert haben, können Sie sie ignorieren.</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `E-Mail-Adresse bestätigen
Hallo {{user_name}},
bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie auf den folgenden Link klicken:
{{verification_url}}
Alternativ können Sie auch diesen Bestätigungscode eingeben: {{verification_code}}
Hinweis: Dieser Link ist nur {{expires_in}} gültig.
Falls Sie diese E-Mail nicht angefordert haben, können Sie sie ignorieren.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getPasswordResetTemplateDE() (string, string, string) {
subject := "Passwort zurücksetzen"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #2563eb;">Passwort zurücksetzen</h1>
<p>Hallo {{user_name}},</p>
<p>Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. Klicken Sie auf den folgenden Link, um ein neues Passwort festzulegen:</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{reset_url}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px;">Neues Passwort festlegen</a>
</p>
<p>Alternativ können Sie auch diesen Code verwenden: <strong>{{reset_code}}</strong></p>
<p><strong>Hinweis:</strong> Dieser Link ist nur {{expires_in}} gültig.</p>
<p style="background-color: #fef3c7; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>Sicherheitshinweis:</strong> Diese Anfrage wurde von der IP-Adresse {{ip_address}} gestellt. Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail und Ihr Passwort bleibt unverändert.
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Passwort zurücksetzen
Hallo {{user_name}},
Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt. Klicken Sie auf den folgenden Link, um ein neues Passwort festzulegen:
{{reset_url}}
Alternativ können Sie auch diesen Code verwenden: {{reset_code}}
Hinweis: Dieser Link ist nur {{expires_in}} gültig.
Sicherheitshinweis: Diese Anfrage wurde von der IP-Adresse {{ip_address}} gestellt. Falls Sie diese Anfrage nicht gestellt haben, ignorieren Sie diese E-Mail und Ihr Passwort bleibt unverändert.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getPasswordChangedTemplateDE() (string, string, string) {
subject := "Ihr Passwort wurde geändert"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #2563eb;">Passwort geändert</h1>
<p>Hallo {{user_name}},</p>
<p>Ihr Passwort wurde am {{changed_at}} erfolgreich geändert.</p>
<p><strong>Details:</strong></p>
<ul>
<li>IP-Adresse: {{ip_address}}</li>
<li>Gerät: {{device_info}}</li>
</ul>
<p style="background-color: #fee2e2; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>Nicht Sie?</strong> Falls Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns sofort unter {{support_url}}.
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Passwort geändert
Hallo {{user_name}},
Ihr Passwort wurde am {{changed_at}} erfolgreich geändert.
Details:
- IP-Adresse: {{ip_address}}
- Gerät: {{device_info}}
Nicht Sie? Falls Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns sofort unter {{support_url}}.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) get2FAEnabledTemplateDE() (string, string, string) {
subject := "Zwei-Faktor-Authentifizierung aktiviert"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #059669;">2FA aktiviert</h1>
<p>Hallo {{user_name}},</p>
<p>Die Zwei-Faktor-Authentifizierung wurde am {{enabled_at}} für Ihr Konto aktiviert.</p>
<p><strong>Gerät:</strong> {{device_info}}</p>
<p style="background-color: #d1fae5; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>Sicherheitstipp:</strong> Bewahren Sie Ihre Recovery-Codes sicher auf. Sie benötigen diese, falls Sie den Zugang zu Ihrer Authenticator-App verlieren.
</p>
<p>Sie können Ihre 2FA-Einstellungen jederzeit unter {{security_url}} verwalten.</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `2FA aktiviert
Hallo {{user_name}},
Die Zwei-Faktor-Authentifizierung wurde am {{enabled_at}} für Ihr Konto aktiviert.
Gerät: {{device_info}}
Sicherheitstipp: Bewahren Sie Ihre Recovery-Codes sicher auf. Sie benötigen diese, falls Sie den Zugang zu Ihrer Authenticator-App verlieren.
Sie können Ihre 2FA-Einstellungen jederzeit unter {{security_url}} verwalten.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) get2FADisabledTemplateDE() (string, string, string) {
subject := "Zwei-Faktor-Authentifizierung deaktiviert"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #dc2626;">2FA deaktiviert</h1>
<p>Hallo {{user_name}},</p>
<p>Die Zwei-Faktor-Authentifizierung wurde am {{disabled_at}} für Ihr Konto deaktiviert.</p>
<p><strong>IP-Adresse:</strong> {{ip_address}}</p>
<p style="background-color: #fee2e2; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>Warnung:</strong> Ihr Konto ist jetzt weniger sicher. Wir empfehlen dringend, 2FA wieder zu aktivieren.
</p>
<p>Sie können 2FA jederzeit unter {{security_url}} wieder aktivieren.</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `2FA deaktiviert
Hallo {{user_name}},
Die Zwei-Faktor-Authentifizierung wurde am {{disabled_at}} für Ihr Konto deaktiviert.
IP-Adresse: {{ip_address}}
Warnung: Ihr Konto ist jetzt weniger sicher. Wir empfehlen dringend, 2FA wieder zu aktivieren.
Sie können 2FA jederzeit unter {{security_url}} wieder aktivieren.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getNewDeviceLoginTemplateDE() (string, string, string) {
subject := "Neuer Login auf Ihrem Konto"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #f59e0b;">Neuer Login erkannt</h1>
<p>Hallo {{user_name}},</p>
<p>Wir haben einen Login auf Ihrem Konto von einem neuen Gerät oder Standort erkannt:</p>
<ul>
<li><strong>Zeitpunkt:</strong> {{login_time}}</li>
<li><strong>IP-Adresse:</strong> {{ip_address}}</li>
<li><strong>Gerät:</strong> {{device_info}}</li>
<li><strong>Standort:</strong> {{location}}</li>
</ul>
<p style="background-color: #fef3c7; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>Nicht Sie?</strong> Falls Sie diesen Login nicht durchgeführt haben, ändern Sie sofort Ihr Passwort unter {{security_url}}.
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Neuer Login erkannt
Hallo {{user_name}},
Wir haben einen Login auf Ihrem Konto von einem neuen Gerät oder Standort erkannt:
- Zeitpunkt: {{login_time}}
- IP-Adresse: {{ip_address}}
- Gerät: {{device_info}}
- Standort: {{location}}
Nicht Sie? Falls Sie diesen Login nicht durchgeführt haben, ändern Sie sofort Ihr Passwort unter {{security_url}}.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getSuspiciousActivityTemplateDE() (string, string, string) {
subject := "Verdächtige Aktivität auf Ihrem Konto"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #dc2626;">Verdächtige Aktivität erkannt</h1>
<p>Hallo {{user_name}},</p>
<p>Wir haben verdächtige Aktivität auf Ihrem Konto festgestellt:</p>
<ul>
<li><strong>Art:</strong> {{activity_type}}</li>
<li><strong>Zeitpunkt:</strong> {{activity_time}}</li>
<li><strong>IP-Adresse:</strong> {{ip_address}}</li>
</ul>
<p style="background-color: #fee2e2; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>Wichtig:</strong> Bitte überprüfen Sie Ihre Sicherheitseinstellungen unter {{security_url}} und ändern Sie Ihr Passwort, falls Sie diese Aktivität nicht selbst durchgeführt haben.
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Verdächtige Aktivität erkannt
Hallo {{user_name}},
Wir haben verdächtige Aktivität auf Ihrem Konto festgestellt:
- Art: {{activity_type}}
- Zeitpunkt: {{activity_time}}
- IP-Adresse: {{ip_address}}
Wichtig: Bitte überprüfen Sie Ihre Sicherheitseinstellungen unter {{security_url}} und ändern Sie Ihr Passwort, falls Sie diese Aktivität nicht selbst durchgeführt haben.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getAccountLockedTemplateDE() (string, string, string) {
subject := "Ihr Konto wurde gesperrt"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #dc2626;">Konto gesperrt</h1>
<p>Hallo {{user_name}},</p>
<p>Ihr Konto wurde am {{locked_at}} aus folgendem Grund gesperrt:</p>
<p style="background-color: #fee2e2; padding: 15px; border-radius: 5px; margin: 20px 0;">
{{reason}}
</p>
<p>Ihr Konto wird automatisch entsperrt am: <strong>{{unlock_time}}</strong></p>
<p>Falls Sie Hilfe benötigen, kontaktieren Sie uns unter {{support_url}}.</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Konto gesperrt
Hallo {{user_name}},
Ihr Konto wurde am {{locked_at}} aus folgendem Grund gesperrt:
{{reason}}
Ihr Konto wird automatisch entsperrt am: {{unlock_time}}
Falls Sie Hilfe benötigen, kontaktieren Sie uns unter {{support_url}}.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getAccountUnlockedTemplateDE() (string, string, string) {
subject := "Ihr Konto wurde entsperrt"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #059669;">Konto entsperrt</h1>
<p>Hallo {{user_name}},</p>
<p>Ihr Konto wurde am {{unlocked_at}} erfolgreich entsperrt.</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{login_url}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px;">Jetzt anmelden</a>
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Konto entsperrt
Hallo {{user_name}},
Ihr Konto wurde am {{unlocked_at}} erfolgreich entsperrt.
Sie können sich jetzt wieder anmelden: {{login_url}}
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getDeletionRequestedTemplateDE() (string, string, string) {
subject := "Bestätigung: Kontolöschung angefordert"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #dc2626;">Kontolöschung angefordert</h1>
<p>Hallo {{user_name}},</p>
<p>Sie haben am {{requested_at}} die Löschung Ihres Kontos beantragt.</p>
<p style="background-color: #fef3c7; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>Wichtig:</strong> Ihr Konto und alle zugehörigen Daten werden endgültig am <strong>{{deletion_date}}</strong> gelöscht.
</p>
<p><strong>Folgende Daten werden gelöscht:</strong></p>
<p>{{data_info}}</p>
<p>Sie können die Löschung bis zum genannten Datum abbrechen:</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{cancel_url}}" style="background-color: #dc2626; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px;">Löschung abbrechen</a>
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Kontolöschung angefordert
Hallo {{user_name}},
Sie haben am {{requested_at}} die Löschung Ihres Kontos beantragt.
Wichtig: Ihr Konto und alle zugehörigen Daten werden endgültig am {{deletion_date}} gelöscht.
Folgende Daten werden gelöscht:
{{data_info}}
Sie können die Löschung bis zum genannten Datum abbrechen: {{cancel_url}}
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getDeletionConfirmedTemplateDE() (string, string, string) {
subject := "Ihr Konto wurde gelöscht"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #6b7280;">Konto gelöscht</h1>
<p>Hallo {{user_name}},</p>
<p>Ihr Konto und alle zugehörigen Daten wurden am {{deleted_at}} erfolgreich und endgültig gelöscht.</p>
<p>Wir bedauern, dass Sie uns verlassen. Falls Sie uns Feedback geben möchten:</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{feedback_url}}" style="background-color: #6b7280; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px;">Feedback geben</a>
</p>
<p>Vielen Dank für Ihre Zeit bei BreakPilot.</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Konto gelöscht
Hallo {{user_name}},
Ihr Konto und alle zugehörigen Daten wurden am {{deleted_at}} erfolgreich und endgültig gelöscht.
Wir bedauern, dass Sie uns verlassen. Falls Sie uns Feedback geben möchten: {{feedback_url}}
Vielen Dank für Ihre Zeit bei BreakPilot.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getDataExportReadyTemplateDE() (string, string, string) {
subject := "Ihr Datenexport ist bereit"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #2563eb;">Datenexport bereit</h1>
<p>Hallo {{user_name}},</p>
<p>Ihr angeforderte Datenexport wurde erstellt und steht zum Download bereit.</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{download_url}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px;">Daten herunterladen ({{file_size}})</a>
</p>
<p style="background-color: #fef3c7; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>Hinweis:</strong> Der Download-Link ist nur {{expires_in}} gültig.
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Datenexport bereit
Hallo {{user_name}},
Ihr angeforderte Datenexport wurde erstellt und steht zum Download bereit:
{{download_url}}
Dateigröße: {{file_size}}
Hinweis: Der Download-Link ist nur {{expires_in}} gültig.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getEmailChangedTemplateDE() (string, string, string) {
subject := "Ihre E-Mail-Adresse wurde geändert"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #2563eb;">E-Mail-Adresse geändert</h1>
<p>Hallo {{user_name}},</p>
<p>Die E-Mail-Adresse Ihres Kontos wurde am {{changed_at}} geändert.</p>
<ul>
<li><strong>Alte Adresse:</strong> {{old_email}}</li>
<li><strong>Neue Adresse:</strong> {{new_email}}</li>
</ul>
<p style="background-color: #fee2e2; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>Nicht Sie?</strong> Falls Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns sofort unter {{support_url}}.
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `E-Mail-Adresse geändert
Hallo {{user_name}},
Die E-Mail-Adresse Ihres Kontos wurde am {{changed_at}} geändert.
- Alte Adresse: {{old_email}}
- Neue Adresse: {{new_email}}
Nicht Sie? Falls Sie diese Änderung nicht vorgenommen haben, kontaktieren Sie uns sofort unter {{support_url}}.
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getNewVersionPublishedTemplateDE() (string, string, string) {
subject := "Neue Version: {{document_name}} - Ihre Zustimmung ist erforderlich"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #2563eb;">Neue Dokumentversion</h1>
<p>Hallo {{user_name}},</p>
<p>Eine neue Version unserer <strong>{{document_name}}</strong> ({{document_type}}) wurde veröffentlicht.</p>
<p><strong>Version:</strong> {{version}}</p>
<p style="background-color: #dbeafe; padding: 15px; border-radius: 5px; margin: 20px 0;">
Bitte prüfen und bestätigen Sie die aktualisierte Version bis zum <strong>{{deadline}}</strong>.
</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{consent_url}}" style="background-color: #2563eb; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px;">Jetzt prüfen und zustimmen</a>
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Neue Dokumentversion
Hallo {{user_name}},
Eine neue Version unserer {{document_name}} ({{document_type}}) wurde veröffentlicht.
Version: {{version}}
Bitte prüfen und bestätigen Sie die aktualisierte Version bis zum {{deadline}}.
Jetzt prüfen und zustimmen: {{consent_url}}
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getConsentReminderTemplateDE() (string, string, string) {
subject := "Erinnerung: Zustimmung zu {{document_name}} erforderlich"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #f59e0b;">Erinnerung: Zustimmung erforderlich</h1>
<p>Hallo {{user_name}},</p>
<p>Dies ist eine freundliche Erinnerung, dass Ihre Zustimmung zur <strong>{{document_name}}</strong> noch aussteht.</p>
<p style="background-color: #fef3c7; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>Noch {{days_left}} Tage</strong> bis zur Frist am {{deadline}}.
</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{consent_url}}" style="background-color: #f59e0b; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px;">Jetzt zustimmen</a>
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Erinnerung: Zustimmung erforderlich
Hallo {{user_name}},
Dies ist eine freundliche Erinnerung, dass Ihre Zustimmung zur {{document_name}} noch aussteht.
Noch {{days_left}} Tage bis zur Frist am {{deadline}}.
Jetzt zustimmen: {{consent_url}}
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getConsentDeadlineWarningTemplateDE() (string, string, string) {
subject := "DRINGEND: Zustimmung zu {{document_name}} läuft bald ab"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #dc2626;">Dringende Erinnerung</h1>
<p>Hallo {{user_name}},</p>
<p>Ihre Frist zur Zustimmung zur <strong>{{document_name}}</strong> läuft in <strong>{{hours_left}}</strong> ab!</p>
<p style="background-color: #fee2e2; padding: 15px; border-radius: 5px; margin: 20px 0;">
<strong>Wichtig:</strong> {{consequences}}
</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{consent_url}}" style="background-color: #dc2626; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px;">Sofort zustimmen</a>
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Dringende Erinnerung
Hallo {{user_name}},
Ihre Frist zur Zustimmung zur {{document_name}} läuft in {{hours_left}} ab!
Wichtig: {{consequences}}
Sofort zustimmen: {{consent_url}}
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
func (s *EmailTemplateService) getAccountSuspendedTemplateDE() (string, string, string) {
subject := "Ihr Konto wurde suspendiert"
bodyHTML := `<!DOCTYPE html>
<html>
<head><meta charset="utf-8"></head>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1 style="color: #dc2626;">Konto suspendiert</h1>
<p>Hallo {{user_name}},</p>
<p>Ihr Konto wurde am {{suspended_at}} suspendiert.</p>
<p><strong>Grund:</strong> {{reason}}</p>
<p><strong>Fehlende Zustimmungen:</strong></p>
<p>{{documents}}</p>
<p>Um Ihr Konto zu reaktivieren, stimmen Sie bitte den ausstehenden Dokumenten zu:</p>
<p style="text-align: center; margin: 30px 0;">
<a href="{{consent_url}}" style="background-color: #dc2626; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px;">Konto reaktivieren</a>
</p>
<p>Mit freundlichen Grüßen,<br>Ihr BreakPilot-Team</p>
</div>
</body>
</html>`
bodyText := `Konto suspendiert
Hallo {{user_name}},
Ihr Konto wurde am {{suspended_at}} suspendiert.
Grund: {{reason}}
Fehlende Zustimmungen:
{{documents}}
Um Ihr Konto zu reaktivieren, stimmen Sie bitte den ausstehenden Dokumenten zu: {{consent_url}}
Mit freundlichen Grüßen,
Ihr BreakPilot-Team`
return subject, bodyHTML, bodyText
}
// InitDefaultTemplates creates default email templates if they don't exist
func (s *EmailTemplateService) InitDefaultTemplates(ctx context.Context) error {
templateTypes := []struct {
Type string
Name string
Description string
SortOrder int
}{
{models.EmailTypeWelcome, "Willkommens-E-Mail", "Wird nach erfolgreicher Registrierung gesendet", 1},
{models.EmailTypeEmailVerification, "E-Mail-Verifizierung", "Enthält Link zur E-Mail-Bestätigung", 2},
{models.EmailTypePasswordReset, "Passwort zurücksetzen", "Enthält Link zum Passwort-Reset", 3},
{models.EmailTypePasswordChanged, "Passwort geändert", "Bestätigung der Passwortänderung", 4},
{models.EmailType2FAEnabled, "2FA aktiviert", "Bestätigung der 2FA-Aktivierung", 5},
{models.EmailType2FADisabled, "2FA deaktiviert", "Warnung über 2FA-Deaktivierung", 6},
{models.EmailTypeNewDeviceLogin, "Neuer Login", "Benachrichtigung über Login von neuem Gerät", 7},
{models.EmailTypeSuspiciousActivity, "Verdächtige Aktivität", "Warnung über verdächtige Kontoaktivität", 8},
{models.EmailTypeAccountLocked, "Konto gesperrt", "Benachrichtigung über Kontosperrung", 9},
{models.EmailTypeAccountUnlocked, "Konto entsperrt", "Bestätigung der Kontoentsperrung", 10},
{models.EmailTypeDeletionRequested, "Löschung angefordert", "Bestätigung der Löschanfrage", 11},
{models.EmailTypeDeletionConfirmed, "Löschung bestätigt", "Bestätigung der Kontolöschung", 12},
{models.EmailTypeDataExportReady, "Datenexport bereit", "Benachrichtigung über fertigen Datenexport", 13},
{models.EmailTypeEmailChanged, "E-Mail geändert", "Bestätigung der E-Mail-Änderung", 14},
{models.EmailTypeNewVersionPublished, "Neue Version veröffentlicht", "Benachrichtigung über neue Dokumentversion", 15},
{models.EmailTypeConsentReminder, "Zustimmungs-Erinnerung", "Erinnerung an ausstehende Zustimmung", 16},
{models.EmailTypeConsentDeadlineWarning, "Frist-Warnung", "Dringende Warnung vor ablaufender Frist", 17},
{models.EmailTypeAccountSuspended, "Konto suspendiert", "Benachrichtigung über Kontosuspendierung", 18},
}
for _, tt := range templateTypes {
// Check if template exists
var exists bool
err := s.db.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM email_templates WHERE type = $1)`, tt.Type).Scan(&exists)
if err != nil {
return fmt.Errorf("failed to check template existence: %w", err)
}
if !exists {
desc := tt.Description
_, err = s.db.Exec(ctx, `
INSERT INTO email_templates (id, type, name, description, is_active, sort_order, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`, uuid.New(), tt.Type, tt.Name, &desc, true, tt.SortOrder, time.Now(), time.Now())
if err != nil {
return fmt.Errorf("failed to create template %s: %w", tt.Type, err)
}
// Create default German version
template, err := s.GetTemplateByType(ctx, tt.Type)
if err != nil {
return fmt.Errorf("failed to get template %s: %w", tt.Type, err)
}
subject, bodyHTML, bodyText := s.GetDefaultTemplateContent(tt.Type, "de")
_, err = s.db.Exec(ctx, `
INSERT INTO email_template_versions
(id, template_id, version, language, subject, body_html, body_text, status, published_at, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
`, uuid.New(), template.ID, "1.0.0", "de", subject, bodyHTML, bodyText, "published", time.Now(), time.Now(), time.Now())
if err != nil {
return fmt.Errorf("failed to create template version %s: %w", tt.Type, err)
}
}
}
return nil
}
// GetSendLogs returns email send logs with optional filtering
func (s *EmailTemplateService) GetSendLogs(ctx context.Context, limit, offset int) ([]models.EmailSendLog, int, error) {
var total int
err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM email_send_logs`).Scan(&total)
if err != nil {
return nil, 0, fmt.Errorf("failed to count send logs: %w", err)
}
rows, err := s.db.Query(ctx, `
SELECT id, user_id, version_id, recipient, subject, status, error_msg, variables, sent_at, delivered_at, created_at
FROM email_send_logs
ORDER BY created_at DESC
LIMIT $1 OFFSET $2
`, limit, offset)
if err != nil {
return nil, 0, fmt.Errorf("failed to get send logs: %w", err)
}
defer rows.Close()
var logs []models.EmailSendLog
for rows.Next() {
var log models.EmailSendLog
err := rows.Scan(&log.ID, &log.UserID, &log.VersionID, &log.Recipient, &log.Subject,
&log.Status, &log.ErrorMsg, &log.Variables, &log.SentAt, &log.DeliveredAt, &log.CreatedAt)
if err != nil {
return nil, 0, fmt.Errorf("failed to scan send log: %w", err)
}
logs = append(logs, log)
}
return logs, total, nil
}
// SendEmail sends an email using the specified template (stub - actual sending would use SMTP)
func (s *EmailTemplateService) SendEmail(ctx context.Context, templateType, language, recipient string, variables map[string]string, userID *uuid.UUID) error {
// Get published version
version, err := s.GetPublishedVersion(ctx, templateType, language)
if err != nil {
return fmt.Errorf("failed to get published version: %w", err)
}
// Render template
rendered, err := s.RenderTemplate(version, variables)
if err != nil {
return fmt.Errorf("failed to render template: %w", err)
}
// Log the send attempt
variablesJSON, _ := json.Marshal(variables)
now := time.Now()
log := &models.EmailSendLog{
ID: uuid.New(),
UserID: userID,
VersionID: version.ID,
Recipient: recipient,
Subject: rendered.Subject,
Status: "queued",
Variables: ptr(string(variablesJSON)),
CreatedAt: now,
}
if err := s.LogEmailSend(ctx, log); err != nil {
return fmt.Errorf("failed to log email send: %w", err)
}
// TODO: Actual email sending via SMTP would go here
// For now, we just log it as "sent"
_, err = s.db.Exec(ctx, `
UPDATE email_send_logs SET status = 'sent', sent_at = $1 WHERE id = $2
`, now, log.ID)
if err != nil {
return fmt.Errorf("failed to update send log status: %w", err)
}
return nil
}
func ptr(s string) *string {
return &s
}