Files
MyClub/internal/controllers/contact_controller.go
T
Tomas Dvorak b9cea0cd77 dev day #79
2025-11-02 01:04:02 +01:00

1320 lines
48 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package controllers
import (
"crypto/rand"
"fmt"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
"fotbal-club/internal/config"
"fotbal-club/internal/models"
"fotbal-club/internal/services"
"fotbal-club/pkg/email"
"fotbal-club/pkg/logger"
"fotbal-club/pkg/utils"
"github.com/gin-gonic/gin"
"gorm.io/datatypes"
"gorm.io/gorm"
"gopkg.in/mail.v2"
)
type ContactController struct {
DB *gorm.DB
emailService email.EmailService
}
func NewContactController(db *gorm.DB, emailService email.EmailService) *ContactController {
return &ContactController{DB: db, emailService: emailService}
}
type NewsletterSubscriptionRequest struct {
Email string `json:"email" binding:"required,email"`
Preferences map[string]bool `json:"preferences"`
}
// SubmitContactForm handles public contact form submissions
// POST /api/v1/contact
func (cc *ContactController) SubmitContactForm(c *gin.Context) {
var input struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required"`
Subject string `json:"subject" binding:"required"`
Message string `json:"message" binding:"required"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"})
return
}
name := strings.TrimSpace(input.Name)
emailStr := strings.TrimSpace(input.Email)
subject := strings.TrimSpace(input.Subject)
message := strings.TrimSpace(input.Message)
if name == "" || emailStr == "" || subject == "" || message == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "All fields are required"})
return
}
if !strings.Contains(emailStr, "@") {
c.JSON(http.StatusBadRequest, gin.H{"error": "Valid email is required"})
return
}
if s, _ := services.FilterBadWords(subject); s != "" {
subject = s
}
if m, _ := services.FilterBadWords(message); m != "" {
message = m
}
ip := c.ClientIP()
ua := c.GetHeader("User-Agent")
msg := models.ContactMessage{
Name: name,
Email: emailStr,
Subject: subject,
Message: message,
Source: "contact",
IPAddress: ip,
UserAgent: ua,
}
if err := cc.DB.Create(&msg).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save message"})
return
}
_ = cc.emailService.SendContactForm(&email.ContactFormData{
Name: name,
Email: emailStr,
Subject: subject,
Message: message,
IPAddress: ip,
UserAgent: ua,
})
c.JSON(http.StatusOK, gin.H{"message": "Message received", "id": msg.ID})
}
func (cc *ContactController) AdminSmtpTest(c *gin.Context) {
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
var input struct {
Host string `json:"host"`
Port int `json:"port"`
Username string `json:"username"`
Password string `json:"password"`
From string `json:"from"`
To string `json:"to"`
Subject string `json:"subject"`
Body string `json:"body"`
UseTLS bool `json:"use_tls"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "Invalid payload"})
return
}
if strings.TrimSpace(input.Host) == "" || input.Port <= 0 || strings.TrimSpace(input.From) == "" || strings.TrimSpace(input.To) == "" {
c.JSON(http.StatusBadRequest, gin.H{"ok": false, "error": "host, port, from and to are required"})
return
}
d := mail.NewDialer(strings.TrimSpace(input.Host), input.Port, strings.TrimSpace(input.Username), input.Password)
d.SSL = input.UseTLS
d.Timeout = 30 * time.Second
subj := strings.TrimSpace(input.Subject)
if subj == "" {
subj = "SMTP Test"
}
body := strings.TrimSpace(input.Body)
if body == "" {
body = "<p>Toto je testovací email SMTP z administrace.</p>"
}
m := mail.NewMessage()
from := strings.TrimSpace(input.From)
to := strings.TrimSpace(input.To)
m.SetHeader("From", from)
m.SetHeader("To", to)
m.SetHeader("Subject", subj)
m.SetDateHeader("Date", time.Now())
m.SetBody("text/plain", "SMTP test email")
m.AddAlternative("text/html", body)
if err := d.DialAndSend(m); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"ok": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "message": "Test email sent"})
}
// GET /api/v1/newsletter/token/me (auth required)
func (cc *ContactController) GetNewsletterTokenForUser(c *gin.Context) {
u, ok := c.Get("user")
if !ok || u == nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Not authenticated"})
return
}
user := u.(*models.User)
email := strings.TrimSpace(strings.ToLower(user.Email))
if email == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "User email not available"})
return
}
var sub models.NewsletterSubscription
if err := cc.DB.Where("email = ?", email).First(&sub).Error; err != nil {
_ = cc.DB.Create(&models.NewsletterSubscription{Email: email, IsActive: true}).Error
} else if !sub.IsActive {
_ = cc.DB.Model(&models.NewsletterSubscription{}).Where("email = ?", email).Update("is_active", true).Error
}
token, err := utils.GenerateSubscriberToken(email, 60*24)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
return
}
c.JSON(http.StatusOK, gin.H{"token": token})
}
// POST /api/v1/admin/newsletter/send-digest
func (cc *ContactController) SendNewsletterDigest(c *gin.Context) {
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
var input struct {
Type string `json:"type" binding:"required"`
Competitions string `json:"competitions"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
return
}
t := strings.ToLower(strings.TrimSpace(input.Type))
allowed := map[string]bool{"blogs": true, "events": true, "matches": true, "scores": true, "weekly": true}
if !allowed[t] {
c.JSON(http.StatusBadRequest, gin.H{"error": "Unknown digest type"})
return
}
var subscribers []models.NewsletterSubscription
if err := cc.DB.Where("is_active = ?", true).Find(&subscribers).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load subscribers"})
return
}
if len(subscribers) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No active subscribers"})
return
}
prefs := services.NewsletterPrefs{Email: "digest@local", ContentTypes: []string{}, Competitions: []string{}, Frequency: "daily"}
if t == "weekly" {
prefs.ContentTypes = []string{"blogs", "events", "matches", "scores"}
prefs.Frequency = "weekly"
} else {
prefs.ContentTypes = []string{t}
}
if strings.TrimSpace(input.Competitions) != "" {
for _, p := range strings.Split(input.Competitions, ",") {
if v := strings.TrimSpace(p); v != "" { prefs.Competitions = append(prefs.Competitions, v) }
}
}
subj, html := services.BuildNewsletterDigest("cache/prefetch", prefs)
if strings.TrimSpace(html) == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "No content for selected digest"})
return
}
recipients := make([]string, 0, len(subscribers))
for _, s := range subscribers { if s.Email != "" { recipients = append(recipients, s.Email) } }
if len(recipients) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "No valid recipient emails"}); return }
if subj == "" { subj = strings.Title(t) + " digest" }
data := &email.NewsletterData{Subject: subj, Content: html, Recipients: recipients}
if err := cc.emailService.SendNewsletter(data); err != nil {
logger.Error("Failed to send digest newsletter: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send digest newsletter"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Digest newsletter sent", "recipients": len(recipients), "type": t})
}
// PATCH /api/v1/admin/newsletter/enable
func (cc *ContactController) UpdateNewsletterAutomation(c *gin.Context) {
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
var input struct { Enabled bool `json:"enabled"` }
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"})
return
}
var s models.Settings
_ = cc.DB.First(&s).Error
if s.ID == 0 { s = models.Settings{} }
s.NewsletterEnabled = input.Enabled
if s.ID == 0 {
if err := cc.DB.Create(&s).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to persist setting"}); return }
} else if err := cc.DB.Save(&s).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to persist setting"}); return }
if config.AppConfig != nil { config.AppConfig.NewsletterEnabled = input.Enabled }
c.JSON(http.StatusOK, gin.H{"newsletter_enabled": input.Enabled})
}
// GET /api/v1/admin/newsletter/status
func (cc *ContactController) GetNewsletterStatus(c *gin.Context) {
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
var total, active int64
cc.DB.Model(&models.NewsletterSubscription{}).Count(&total)
cc.DB.Model(&models.NewsletterSubscription{}).Where("is_active = ?", true).Count(&active)
var subs []models.NewsletterSubscription
_ = cc.DB.Where("is_active = ?", true).Limit(20).Find(&subs).Error
sample := make([]string, 0, len(subs))
for _, s := range subs { if s.Email != "" { sample = append(sample, s.Email) } }
interval := 24 * time.Hour
if v := strings.TrimSpace(os.Getenv("NEWSLETTER_INTERVAL_HOURS")); v != "" {
if d, err := time.ParseDuration(v + "h"); err == nil { interval = d }
}
next := time.Now().Add(interval)
c.JSON(http.StatusOK, gin.H{"total_subscribers": total, "active_subscribers": active, "sample_recipients": sample, "interval_minutes": int(interval.Minutes()), "next_approximate": next, "newsletter_enabled": config.AppConfig != nil && config.AppConfig.NewsletterEnabled})
}
// POST /api/v1/admin/newsletter/preview
func (cc *ContactController) PreviewNewsletter(c *gin.Context) {
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
var input struct { Preferences map[string]interface{} `json:"preferences"` }
_ = c.ShouldBindJSON(&input)
prefs := services.NewsletterPrefs{Email: "preview@local", ContentTypes: []string{}, Competitions: []string{}, Frequency: "daily"}
if m := input.Preferences; m != nil {
if b, ok := m["blogs"].(bool); ok && b { prefs.ContentTypes = append(prefs.ContentTypes, "blogs") }
if b, ok := m["events"].(bool); ok && b { prefs.ContentTypes = append(prefs.ContentTypes, "events") }
if b, ok := m["matches"].(bool); ok && b { prefs.ContentTypes = append(prefs.ContentTypes, "matches") }
if b, ok := m["scores"].(bool); ok && b { prefs.ContentTypes = append(prefs.ContentTypes, "scores") }
if cs, ok := m["competitions"].(string); ok && strings.TrimSpace(cs) != "" {
for _, p := range strings.Split(cs, ",") { if v := strings.TrimSpace(p); v != "" { prefs.Competitions = append(prefs.Competitions, v) } }
}
}
subj, html := services.BuildNewsletterDigest("cache/prefetch", prefs)
c.JSON(http.StatusOK, gin.H{"subject": subj, "html": html})
}
// GET /api/v1/newsletter/preferences
func (cc *ContactController) GetNewsletterPreferencesByToken(c *gin.Context) {
token := strings.TrimSpace(c.Query("token"))
if token == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "token is required"}); return }
emailStr, err := utils.ParseSubscriberToken(token)
if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or expired token"}); return }
var sub models.NewsletterSubscription
if err := cc.DB.Where("email = ?", emailStr).First(&sub).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Subscription not found"}); return }
c.JSON(http.StatusOK, gin.H{"email": sub.Email, "is_active": sub.IsActive, "preferences": sub.Preferences})
}
// POST /api/v1/newsletter/preferences
func (cc *ContactController) SaveNewsletterPreferencesByToken(c *gin.Context) {
var input struct { Token string `json:"token" binding:"required"`; Preferences map[string]interface{} `json:"preferences" binding:"required"` }
if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"}); return }
emailStr, err := utils.ParseSubscriberToken(input.Token)
if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or expired token"}); return }
var sub models.NewsletterSubscription
if err := cc.DB.Where("email = ?", emailStr).First(&sub).Error; err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Subscription not found"}); return }
jm := datatypes.JSONMap{}
for key, raw := range input.Preferences {
switch v := raw.(type) {
case bool:
jm[key] = v
case string:
jm[key] = strings.TrimSpace(v)
case []interface{}:
compiled := make([]string, 0, len(v))
for _, item := range v { if s, ok := item.(string); ok { if trimmed := strings.TrimSpace(s); trimmed != "" { compiled = append(compiled, trimmed) } } }
jm[key] = strings.Join(compiled, ", ")
case float64, int, int64:
jm[key] = v
case nil:
jm[key] = nil
default:
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Unsupported preference type for %s", key)})
return
}
}
if compVal, ok := jm["competitions"]; ok {
if compStr, ok := compVal.(string); ok {
comp := strings.TrimSpace(compStr)
jm["competitions"] = comp
if comp != "" {
if catVal, exists := jm["categories"]; !exists {
jm["categories"] = comp
} else if catStr, ok := catVal.(string); ok && strings.TrimSpace(catStr) == "" {
jm["categories"] = comp
}
}
}
}
sub.Preferences = jm
sub.UpdatedAt = time.Now()
if err := cc.DB.Save(&sub).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save preferences"}); return }
c.JSON(http.StatusOK, gin.H{"message": "Preferences saved"})
}
// POST /api/v1/newsletter/unsubscribe-token
func (cc *ContactController) UnsubscribeByToken(c *gin.Context) {
var input struct { Token string `json:"token" binding:"required"` }
if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"}); return }
emailStr, err := utils.ParseSubscriberToken(input.Token)
if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or expired token"}); return }
if err := cc.DB.Model(&models.NewsletterSubscription{}).Where("email = ?", emailStr).Update("is_active", false).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unsubscribe"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "You have been unsubscribed"})
}
// DELETE /api/v1/admin/newsletter/subscribers/:id
func (cc *ContactController) DeleteNewsletterSubscriber(c *gin.Context) {
if c.GetString("userRole") != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}); return }
id, err := strconv.Atoi(c.Param("id"))
if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid subscriber ID"}); return }
result := cc.DB.Delete(&models.NewsletterSubscription{}, id)
if result.Error != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete subscriber"}); return }
if result.RowsAffected == 0 { c.JSON(http.StatusNotFound, gin.H{"error": "Subscriber not found"}); return }
c.JSON(http.StatusOK, gin.H{"message": "Subscriber deleted successfully"})
}
// PATCH /api/v1/admin/newsletter/subscribers/:id/status
func (cc *ContactController) UpdateNewsletterSubscriberStatus(c *gin.Context) {
if c.GetString("userRole") != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}); return }
id, err := strconv.Atoi(c.Param("id"))
if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid subscriber ID"}); return }
var input struct { IsActive bool `json:"is_active" binding:"required"` }
if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"}); return }
var sub models.NewsletterSubscription
if err := cc.DB.First(&sub, id).Error; err != nil {
if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Subscriber not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch subscriber"}) }
return
}
sub.IsActive = input.IsActive
sub.UpdatedAt = time.Now()
if err := cc.DB.Save(&sub).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update subscriber"}); return }
c.JSON(http.StatusOK, sub)
}
// PATCH /api/v1/admin/newsletter/subscribers/:id/preferences
func (cc *ContactController) UpdateNewsletterSubscriberPreferences(c *gin.Context) {
if c.GetString("userRole") != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}); return }
id, err := strconv.Atoi(c.Param("id"))
if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid subscriber ID"}); return }
var prefs map[string]bool
if err := c.ShouldBindJSON(&prefs); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid preferences payload"}); return }
var sub models.NewsletterSubscription
if err := cc.DB.First(&sub, id).Error; err != nil {
if err == gorm.ErrRecordNotFound { c.JSON(http.StatusNotFound, gin.H{"error": "Subscriber not found"}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch subscriber"}) }
return
}
jm := datatypes.JSONMap{}
for k, v := range prefs { jm[k] = v }
sub.Preferences = jm
sub.UpdatedAt = time.Now()
if err := cc.DB.Save(&sub).Error; err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update preferences"}); return }
c.JSON(http.StatusOK, sub)
}
// POST /api/v1/admin/newsletter/test
func (cc *ContactController) SendNewsletterTest(c *gin.Context) {
if c.GetString("userRole") != "admin" { c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"}); return }
var input struct { Email string `json:"email"`; Emails []string `json:"emails"`; Type string `json:"type"` }
_ = c.ShouldBindJSON(&input)
recipients := make([]string, 0)
if len(input.Emails) > 0 { for _, e := range input.Emails { if v := strings.TrimSpace(e); v != "" { recipients = append(recipients, v) } } }
if len(recipients) == 0 { if v := strings.TrimSpace(input.Email); v != "" { recipients = append(recipients, v) } }
if len(recipients) == 0 { if v := strings.TrimSpace(config.AppConfig.AdminEmail); v != "" { recipients = append(recipients, v) } }
if len(recipients) == 0 { c.JSON(http.StatusBadRequest, gin.H{"error": "No recipient email provided"}); return }
t := strings.ToLower(strings.TrimSpace(input.Type)); if t == "" { t = "newsletter" }
logger.Info("[SendNewsletterTest] type=%s recipients=%v", t, recipients)
switch t {
case "newsletter":
testHTML := `<p>Toto je testovací newsletter z Fotbal Club. Nastavení SMTP funguje.</p>`
data := &email.NewsletterData{Subject: "Test newsletter", Content: testHTML, Recipients: recipients}
logger.Debug("[SendNewsletterTest] invoking emailService.SendNewsletter for %d recipient(s)", len(recipients))
if err := cc.emailService.SendNewsletter(data); err != nil {
logger.Error("Failed to send test newsletter: %v", err)
if config.AppConfig != nil && config.AppConfig.AppEnv != "production" { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send test newsletter", "details": err.Error()}) } else { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send test newsletter"}) }
return
}
case "welcome":
for _, r := range recipients { _ = cc.emailService.SendNewsletterWelcome(&email.NewsletterWelcomeData{Email: r}) }
case "welcome_back":
for _, r := range recipients { _ = cc.emailService.SendNewsletterWelcomeBack(&email.NewsletterWelcomeBackData{Email: r}) }
case "setup":
for _, r := range recipients {
token, _ := utils.GenerateSubscriberToken(r, 60*24) // 1 day
baseFE := strings.TrimSuffix(config.AppConfig.FrontendBaseURL, "/")
setupURL := baseFE + "/newsletter/setup?token=" + url.QueryEscape(token)
// Engagement: if a user already exists for this email, award points
var subUser models.User
if err := cc.DB.Where("LOWER(email) = LOWER(?)", r).First(&subUser).Error; err == nil && subUser.ID != 0 {
es := services.NewEngagementService(cc.DB)
_, _ = es.AwardPoints(subUser.ID, 12, "newsletter_subscribe", map[string]interface{}{"email": r})
_ = es.CheckAndAwardAchievements(subUser.ID)
}
setupEmail := &email.EmailData{
Subject: "Nastavte svůj newsletter",
To: []string{r},
Template: "newsletter_setup",
Data: struct{ SetupURL string }{SetupURL: setupURL},
}
_ = cc.emailService.SendEmail(setupEmail)
}
case "blogs", "events", "matches", "scores", "weekly":
prefs := services.NewsletterPrefs{Email: recipients[0], ContentTypes: []string{}, Competitions: []string{}, Frequency: "daily"}
if t == "weekly" { prefs.ContentTypes = []string{"blogs", "events", "matches", "scores"}; prefs.Frequency = "weekly" } else { prefs.ContentTypes = []string{t} }
subj, html := services.BuildNewsletterDigest("cache/prefetch", prefs)
if subj == "" { subj = "Test digest" }
if html == "" { html = "<p>Momentálně žádný obsah pro zvolený typ.</p>" }
data := &email.NewsletterData{Subject: subj, Content: html, Recipients: recipients}
if err := cc.emailService.SendNewsletter(data); err != nil { logger.Error("Failed to send digest test: %v", err); c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send digest test"}); return }
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "Unknown test type"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Test email(s) sent", "recipients": recipients, "type": t})
}
// SubscribeToNewsletter handles newsletter subscriptions
// @Summary Subscribe to newsletter
// @Description Handles newsletter subscription requests
// @Tags newsletter
// @Accept json
// @Produce json
// @Param input body NewsletterSubscriptionRequest true "Subscription data"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Router /api/v1/newsletter/subscribe [post]
func (cc *ContactController) SubscribeToNewsletter(c *gin.Context) {
var input NewsletterSubscriptionRequest
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Check if email already exists
var subscription models.NewsletterSubscription
result := cc.DB.Where("email = ?", input.Email).First(&subscription)
if result.Error == nil {
if !subscription.IsActive {
// Reactivate existing subscription
subscription.IsActive = true
subscription.UpdatedAt = time.Now()
// Update preferences if provided (convert to JSONMap)
if input.Preferences != nil {
jm := datatypes.JSONMap{}
for k, v := range input.Preferences {
jm[k] = v
}
subscription.Preferences = jm
}
if err := cc.DB.Save(&subscription).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update subscription"})
return
}
// Engagement: award points to matching user (if exists)
var u models.User
if err := cc.DB.Where("LOWER(email) = LOWER(?)", subscription.Email).First(&u).Error; err == nil && u.ID != 0 {
es := services.NewEngagementService(cc.DB)
_, _ = es.AwardPoints(u.ID, 12, "newsletter_subscribe", map[string]interface{}{"email": subscription.Email})
_ = es.CheckAndAwardAchievements(u.ID)
}
// Send welcome back email in a goroutine
go func(sub models.NewsletterSubscription) {
manageURL := ""
unsubscribeURL := ""
token, tErr := utils.GenerateSubscriberToken(sub.Email, 60*24)
if tErr != nil {
logger.Error("Failed to generate subscriber token: %v", tErr)
} else {
baseFE := strings.TrimSuffix(config.AppConfig.FrontendBaseURL, "/")
if baseFE != "" {
link := baseFE + "/newsletter/preferences?token=" + url.QueryEscape(token)
manageURL = link
unsubscribeURL = link
}
}
emailData := &email.NewsletterWelcomeBackData{
Email: sub.Email,
ManageURL: manageURL,
UnsubscribeURL: unsubscribeURL,
}
if err := cc.emailService.SendNewsletterWelcomeBack(emailData); err != nil {
logger.Error("Failed to send welcome back email: %v", err)
}
}(subscription)
c.JSON(http.StatusOK, gin.H{"message": "Welcome back! You have been resubscribed to our newsletter."})
return
}
c.JSON(http.StatusOK, gin.H{"message": "You are already subscribed to our newsletter"})
return
}
// Create new subscription. Default: enable everything if preferences omitted
prefs := input.Preferences
if prefs == nil {
prefs = map[string]bool{"weekly": true, "matches": true, "blogs": true, "events": true, "scores": true}
}
// convert to datatypes.JSONMap
jm := datatypes.JSONMap{}
for k, v := range prefs {
jm[k] = v
}
subscription = models.NewsletterSubscription{
Email: input.Email,
IsActive: true,
Preferences: jm,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Generate a subscriber token to include in follow-up emails (preferences links)
token, _ := utils.GenerateSubscriberToken(subscription.Email, 60*24) // 1 day
baseFE := strings.TrimSuffix(config.AppConfig.FrontendBaseURL, "/")
setupURL := baseFE + "/newsletter/setup?token=" + url.QueryEscape(token)
unsubscribeURL := baseFE + "/newsletter/preferences?token=" + url.QueryEscape(token)
// Engagement: if a user already exists for this email, award points
var u models.User
if err := cc.DB.Where("LOWER(email) = LOWER(?)", subscription.Email).First(&u).Error; err == nil && u.ID != 0 {
es := services.NewEngagementService(cc.DB)
_, _ = es.AwardPointsCapped(u.ID, 12, "newsletter_subscribe", map[string]interface{}{"email": subscription.Email})
_ = es.CheckAndAwardAchievements(u.ID)
}
// Auto-create fan user account if not exists
var existingUser models.User
if err := cc.DB.Where("LOWER(email) = LOWER(?)", subscription.Email).First(&existingUser).Error; err == gorm.ErrRecordNotFound {
// Generate a strong random password (16 chars, mixed set)
genPassword := func(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyz"
const upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
const digits = "0123456789"
const symbols = "!@#$%^&*()-_=+[]{}?"
pool := letters + upper + digits + symbols
b := make([]byte, n)
for i := 0; i < n; i++ {
var rb [1]byte
if _, err := rand.Read(rb[:]); err != nil {
b[i] = pool[(time.Now().UnixNano()+int64(i))%int64(len(pool))]
continue
}
b[i] = pool[int(rb[0])%len(pool)]
}
return string(b)
}
plain := genPassword(16)
hashed, hErr := utils.HashPassword(plain)
if hErr == nil {
u := models.User{
Email: strings.TrimSpace(strings.ToLower(subscription.Email)),
Password: hashed,
FirstName: "",
LastName: "",
Role: "fan",
IsActive: true,
}
if err := cc.DB.Create(&u).Error; err != nil {
logger.Error("Failed to auto-create fan user for newsletter: %v", err)
} else {
// Send credentials email
data := map[string]interface{}{
"Email": subscription.Email,
"Password": plain,
"LoginURL": baseFE + "/login",
"ResetURL": baseFE + "/forgot-password",
"ManageURL": setupURL,
"UnsubscribeURL": unsubscribeURL,
}
credEmail := &email.EmailData{
Subject: "Váš fan účet byl vytvořen",
To: []string{subscription.Email},
Template: "fan_account_created",
Data: data,
}
if err := cc.emailService.SendEmail(credEmail); err != nil {
logger.Error("Failed to send fan account created email: %v", err)
}
// Engagement: award points to new user
es := services.NewEngagementService(cc.DB)
_, _ = es.AwardPoints(u.ID, 12, "newsletter_subscribe", map[string]interface{}{"email": subscription.Email})
_ = es.CheckAndAwardAchievements(u.ID)
}
}
}
// Send setup email (link with token) AND welcome introduction email in goroutines
go func() {
// 1) Setup email
setupEmail := &email.EmailData{
Subject: "Nastavte svůj newsletter",
To: []string{subscription.Email},
Template: "newsletter_setup",
Data: struct{ SetupURL string }{SetupURL: setupURL},
}
if err := cc.emailService.SendEmail(setupEmail); err != nil {
logger.Error("Failed to send setup email: %v", err)
}
// 2) Welcome introduction email (includes unsubscribe/manage link)
welcome := &email.NewsletterWelcomeData{
Email: subscription.Email,
UnsubscribeLink: unsubscribeURL,
}
if err := cc.emailService.SendNewsletterWelcome(welcome); err != nil {
logger.Error("Failed to send welcome email: %v", err)
}
}()
c.JSON(http.StatusOK, gin.H{"message": "Thank you for subscribing to our newsletter! Please check your email to confirm your subscription."})
}
// SetupNewsletterPreferences accepts a subscriber token and preferences to save choices
// ... (rest of the code remains the same)
// @Summary Setup newsletter preferences
// @Description Accepts token and preferences to save subscriber choices
// @Accept json
// @Produce json
// @Param input body map[string]interface{} true "{ token: string, preferences: { .. } }"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Router /api/v1/newsletter/setup [post]
func (cc *ContactController) SetupNewsletterPreferences(c *gin.Context) {
var input struct {
Token string `json:"token" binding:"required"`
Preferences map[string]bool `json:"preferences" binding:"required"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"})
return
}
emailStr, err := utils.ParseSubscriberToken(input.Token)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid or expired token"})
return
}
var sub models.NewsletterSubscription
if err := cc.DB.Where("email = ?", emailStr).First(&sub).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Subscription not found"})
return
}
// convert map[string]bool to datatypes.JSONMap
jm := datatypes.JSONMap{}
for k, v := range input.Preferences {
jm[k] = v
}
sub.Preferences = jm
sub.UpdatedAt = time.Now()
if err := cc.DB.Save(&sub).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save preferences"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Preferences saved"})
}
// GetContactMessages returns a list of contact messages (admin only)
// @Summary Get contact messages
// @Description Returns a paginated list of contact messages (admin only)
// @Tags admin
// @Security Bearer
// @Produce json
// @Param page query int false "Page number" default(1)
// @Param limit query int false "Items per page" default(10)
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/contact-messages [get]
func (cc *ContactController) GetContactMessages(c *gin.Context) {
// Check if user is admin
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
// Get pagination parameters
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "10"))
offset := (page - 1) * limit
var messages []models.ContactMessage
var total int64
// Get total count
if err := cc.DB.Model(&models.ContactMessage{}).Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch messages"})
return
}
// Get paginated messages
if err := cc.DB.Offset(offset).Limit(limit).Order("created_at DESC").Find(&messages).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch messages"})
return
}
c.JSON(http.StatusOK, gin.H{
"data": messages,
"pagination": gin.H{
"total": total,
"page": page,
"limit": limit,
"pages": (int(total) + limit - 1) / limit,
"has_more": offset+limit < int(total),
},
})
}
// MarkMessageAsRead marks a contact message as read (admin only)
// @Summary Mark message as read
// @Description Marks a contact message as read (admin only)
// @Tags admin
// @Security Bearer
// @Produce json
// @Param id path int true "Message ID"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/contact-messages/{id}/read [patch]
func (cc *ContactController) MarkMessageAsRead(c *gin.Context) {
// Check if user is admin
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
var message models.ContactMessage
if err := cc.DB.First(&message, c.Param("id")).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "Message not found"})
return
}
message.IsRead = true
message.ReadAt = time.Now()
if err := cc.DB.Save(&message).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update message"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Message marked as read"})
}
// GetNewsletterSubscribers returns a list of newsletter subscribers (admin only)
// @Summary Get newsletter subscribers
// @Description Returns a list of all newsletter subscribers (admin only)
// @Tags admin
// @Security Bearer
// @Produce json
// @Success 200 {object} []models.NewsletterSubscription
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/newsletter/subscribers [get]
func (cc *ContactController) GetNewsletterSubscribers(c *gin.Context) {
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
var subscribers []models.NewsletterSubscription
if err := cc.DB.Find(&subscribers).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch subscribers"})
return
}
c.JSON(http.StatusOK, subscribers)
}
// SendNewsletter sends a newsletter to all active subscribers (admin only)
// @Summary Send newsletter
// @Description Sends a newsletter to all active subscribers (admin only)
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @Param input body map[string]string true "Newsletter content (subject and body)"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/newsletter/send [post]
func (cc *ContactController) SendNewsletter(c *gin.Context) {
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
var input struct {
Subject string `json:"subject" binding:"required"`
Body string `json:"body"`
Content string `json:"content"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid input"})
return
}
// Support both 'body' (backend) and 'content' (frontend variant)
bodyText := strings.TrimSpace(input.Body)
if bodyText == "" {
bodyText = strings.TrimSpace(input.Content)
}
if bodyText == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Newsletter body/content is required"})
return
}
// Fetch active subscribers
var subscribers []models.NewsletterSubscription
if err := cc.DB.Where("is_active = ?", true).Find(&subscribers).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to load subscribers"})
return
}
if len(subscribers) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No active subscribers"})
return
}
// Build recipient list
recipients := make([]string, 0, len(subscribers))
for _, s := range subscribers {
if s.Email != "" {
recipients = append(recipients, s.Email)
}
}
if len(recipients) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No valid recipient emails"})
return
}
logger.Info("[SendNewsletter] sending to %d active subscribers", len(recipients))
// Send via email service
data := &email.NewsletterData{
Subject: input.Subject,
Content: bodyText,
Recipients: recipients,
}
if err := cc.emailService.SendNewsletter(data); err != nil {
logger.Error("Failed to send newsletter: %v", err)
if config.AppConfig != nil && config.AppConfig.AppEnv != "production" {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send newsletter", "details": err.Error()})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to send newsletter"})
}
return
}
c.JSON(http.StatusOK, gin.H{"message": "Newsletter sent successfully", "recipients": len(recipients)})
}
// UnsubscribeFromNewsletter handles newsletter unsubscription
// @Summary Unsubscribe from newsletter
// @Description Handles newsletter unsubscription requests
// @Tags newsletter
// @Produce json
// @Param email path string true "Subscriber email"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Router /api/v1/newsletter/unsubscribe/{email} [post]
func (cc *ContactController) UnsubscribeFromNewsletter(c *gin.Context) {
email := c.Param("email")
if email == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Email is required"})
return
}
// Set subscription as inactive instead of deleting
result := cc.DB.Model(&models.NewsletterSubscription{}).Where("email = ?", email).Update("is_active", false)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to unsubscribe"})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Subscription not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Successfully unsubscribed from newsletter"})
}
// GetContactMessage returns a single contact message by ID (admin only)
// @Summary Get contact message
// @Description Returns a single contact message by ID (admin only)
// @Tags admin
// @Security Bearer
// @Produce json
// @Param id path int true "Message ID"
// @Success 200 {object} models.ContactMessage
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/contact-messages/{id} [get]
func (cc *ContactController) GetContactMessage(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid message ID"})
return
}
var message models.ContactMessage
if err := cc.DB.First(&message, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Message not found"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch message"})
}
return
}
c.JSON(http.StatusOK, message)
}
// DeleteContactMessage deletes a contact message (admin only)
// @Summary Delete contact message
// @Description Deletes a contact message by ID (admin only)
// @Tags admin
// @Security Bearer
// @Produce json
// @Param id path int true "Message ID"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/contact-messages/{id} [delete]
func (cc *ContactController) DeleteContactMessage(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid message ID"})
return
}
result := cc.DB.Delete(&models.ContactMessage{}, id)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete message"})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "Message not found"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Message deleted successfully"})
}
// DeleteContactMessages deletes multiple contact messages (admin only)
// @Summary Delete multiple contact messages
// @Description Deletes multiple contact messages by their IDs (admin only)
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @Param ids body []int true "Array of message IDs to delete"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/contact-messages [delete]
func (cc *ContactController) DeleteContactMessages(c *gin.Context) {
var ids []int
if err := c.ShouldBindJSON(&ids); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body"})
return
}
if len(ids) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No message IDs provided"})
return
}
result := cc.DB.Delete(&models.ContactMessage{}, ids)
if result.Error != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete messages"})
return
}
if result.RowsAffected == 0 {
c.JSON(http.StatusNotFound, gin.H{"error": "No messages found with the provided IDs"})
return
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Successfully deleted %d message(s)", result.RowsAffected),
})
}
// ForwardContactMessage forwards a contact message to a specified email (admin only)
// @Summary Forward contact message
// @Description Forwards a contact message to a specified email address (admin only)
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @Param id path int true "Message ID"
// @Param input body map[string]string true "{ to_email: string }"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Failure 404 {object} map[string]string
// @Router /api/v1/admin/contact-messages/{id}/forward [post]
func (cc *ContactController) ForwardContactMessage(c *gin.Context) {
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid message ID"})
return
}
var input struct {
ToEmail string `json:"to_email" binding:"required,email"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Valid email address is required"})
return
}
// Fetch the message
var message models.ContactMessage
if err := cc.DB.First(&message, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, gin.H{"error": "Message not found"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
}
return
}
// Prepare email data for forwarding (Czech subject)
forwardData := &email.EmailData{
Subject: fmt.Sprintf("Přeposláno: Kontaktní formulář - %s", message.Subject),
To: []string{input.ToEmail},
Template: "contact_form",
Data: struct {
Name string
Email string
Subject string
Message string
Time string
IP string
Agent string
}{
Name: message.Name,
Email: message.Email,
Subject: message.Subject,
Message: message.Message,
Time: message.CreatedAt.Format(time.RFC1123Z),
IP: message.IPAddress,
Agent: message.UserAgent,
},
}
if err := cc.emailService.SendEmail(forwardData); err != nil {
logger.Error("Failed to forward contact message %d to %s: %v", message.ID, input.ToEmail, err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to forward message"})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Message forwarded"})
}
// @Summary Forward all contact messages
// @Description Forwards all contact messages to a specified email address (admin only)
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @Param input body map[string]string true "{ to_email: string, to_emails: []string, save_default: bool }"
// @Success 200 {object} map[string]string
// @Failure 400 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/contact-messages/forward-all [post]
func (cc *ContactController) ForwardAllContactMessages(c *gin.Context) {
if c.GetString("userRole") != "admin" {
c.JSON(http.StatusForbidden, gin.H{"error": "Access denied"})
return
}
var input struct {
ToEmail string `json:"to_email"`
ToEmails []string `json:"to_emails"`
SaveDefault bool `json:"save_default"`
}
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload"})
return
}
// Build recipients list (supports comma/semicolon/space separated string or array)
recipients := make([]string, 0)
add := func(s string) {
v := strings.TrimSpace(s)
if v != "" {
recipients = append(recipients, v)
}
}
if len(input.ToEmails) > 0 {
for _, e := range input.ToEmails {
add(e)
}
}
if input.ToEmail != "" {
// split by common separators to allow multiple addresses in a single string
parts := strings.FieldsFunc(input.ToEmail, func(r rune) bool { return r == ',' || r == ';' || r == ' ' || r == '\n' || r == '\t' })
if len(parts) > 1 {
for _, p := range parts {
add(p)
}
} else {
add(input.ToEmail)
}
}
// Deduplicate
if len(recipients) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "No recipient email provided"})
return
}
uniq := make(map[string]struct{})
out := make([]string, 0, len(recipients))
for _, e := range recipients {
v := strings.TrimSpace(strings.ToLower(e))
if v == "" {
continue
}
if _, ok := uniq[v]; ok {
continue
}
uniq[v] = struct{}{}
out = append(out, e)
}
recipients = out
// Optionally save as default auto-forward list in Settings
if input.SaveDefault {
var set models.Settings
if err := cc.DB.First(&set).Error; err != nil {
if err == gorm.ErrRecordNotFound {
set = models.Settings{}
set.ContactForwardEnabled = true
set.ContactForwardList = strings.Join(recipients, ", ")
_ = cc.DB.Create(&set).Error
}
} else {
set.ContactForwardEnabled = true
set.ContactForwardList = strings.Join(recipients, ", ")
_ = cc.DB.Save(&set).Error
}
}
// Fetch all messages
var messages []models.ContactMessage
if err := cc.DB.Order("created_at DESC").Find(&messages).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch messages"})
return
}
if len(messages) == 0 {
// Even if there are no messages now, ensure auto-forward is configured
if !input.SaveDefault {
var set models.Settings
if err := cc.DB.First(&set).Error; err != nil {
if err == gorm.ErrRecordNotFound {
set = models.Settings{}
set.ContactForwardEnabled = true
set.ContactForwardList = strings.Join(recipients, ", ")
_ = cc.DB.Create(&set).Error
}
} else {
set.ContactForwardEnabled = true
set.ContactForwardList = strings.Join(recipients, ", ")
_ = cc.DB.Save(&set).Error
}
}
c.JSON(http.StatusOK, gin.H{
"message": "Automatické přeposílání je nastaveno. Zatím nejsou žádné zprávy k přeposlání.",
"count": 0,
})
return
}
// Forward all messages asynchronously
go func(msgs []models.ContactMessage, dest []string) {
successCount := 0
for _, message := range msgs {
forwardData := &email.EmailData{
Subject: fmt.Sprintf("Přeposláno: Kontaktní formulář - %s", message.Subject),
To: dest,
Template: "contact_form",
Data: struct {
Name string
Email string
Subject string
Message string
Time string
IP string
Agent string
}{
Name: message.Name,
Email: message.Email,
Subject: message.Subject,
Message: message.Message,
Time: message.CreatedAt.Format(time.RFC1123Z),
IP: message.IPAddress,
Agent: message.UserAgent,
},
}
if err := cc.emailService.SendEmail(forwardData); err != nil {
logger.Error("Failed to forward contact message %d to %v: %v", message.ID, dest, err)
} else {
successCount++
}
}
logger.Info("Forwarded %d of %d contact messages to %v", successCount, len(msgs), dest)
}(messages, recipients)
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("Přeposílám %d zpráv na: %s", len(messages), strings.Join(recipients, ", ")),
"count": len(messages),
})
}