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>
1674 lines
62 KiB
Go
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
|
|
}
|