@@ -20,6 +20,7 @@ import (
"github.com/gin-gonic/gin"
"gorm.io/datatypes"
"gorm.io/gorm"
"gopkg.in/mail.v2"
)
type ContactController struct {
@@ -27,7 +28,61 @@ type ContactController struct {
emailService email . EmailService
}
// GetNewsletterTokenForUser returns a short-lived newsletter preferences token for the authenticated user's email
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í e‑ mail 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" )
@@ -43,24 +98,27 @@ func (cc *ContactController) GetNewsletterTokenForUser(c *gin.Context) {
return
}
// Generate a 24h token for managing newsletter preferences
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 } )
}
// SendNewsletterDigest builds and sends a digest newsletter based on a template type (admin only)
// POST /api/v1/admin/newsletter/send-digest { type: "blogs|events|matches|scores|weekly", competitions?: "ABC, DEF" }
// 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" `
@@ -69,7 +127,6 @@ func (cc *ContactController) SendNewsletterDigest(c *gin.Context) {
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 ] {
@@ -77,7 +134,6 @@ func (cc *ContactController) SendNewsletterDigest(c *gin.Context) {
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" } )
@@ -88,13 +144,7 @@ func (cc *ContactController) SendNewsletterDigest(c *gin.Context) {
return
}
// Build digest content once based on selected type
prefs := services . NewsletterPrefs {
Email : "digest@local" ,
ContentTypes : [ ] string { } ,
Competitions : [ ] string { } ,
Frequency : "daily" ,
}
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"
@@ -103,9 +153,7 @@ func (cc *ContactController) SendNewsletterDigest(c *gin.Context) {
}
if strings . TrimSpace ( input . Competitions ) != "" {
for _ , p := range strings . Split ( input . Competitions , "," ) {
if v := strings . TrimSpace ( p ) ; v != "" {
prefs . Competitions = append ( prefs . Competitions , v )
}
if v := strings . TrimSpace ( p ) ; v != "" { prefs . Competitions = append ( prefs . Competitions , v ) }
}
}
subj , html := services . BuildNewsletterDigest ( "cache/prefetch" , prefs )
@@ -113,227 +161,103 @@ func (cc *ContactController) SendNewsletterDigest(c *gin.Context) {
c . JSON ( http . StatusBadRequest , gin . H { "error" : "No content for selected digest" } )
return
}
// Recipients 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
}
if subj == "" {
subj = strings . Title ( t ) + " digest"
}
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 } )
}
// UpdateNewsletterAutomation toggles the automated newsletter scheduler at runtime (non-persistent)
// PATCH /api/v1/admin/newsletter/enable { enabled: boolean }
// 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" `
}
var input struct { Enabled bool ` json:"enabled" ` }
if err := c . ShouldBindJSON ( & input ) ; err != nil {
c . JSON ( http . StatusBadRequest , gin . H { "error" : "Invalid payload" } )
return
}
// Persist to Settings (singleton row)
var s models . Settings
_ = cc . DB . First ( & s ) . Error // ignore not found
if s . ID == 0 {
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
}
// Flip the in-memory config flag; effective immediately for next tick
if config . AppConfig != nil {
config . AppConfig . NewsletterEnabled = input . Enabled
}
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 } )
}
// GetNewsletterStatus returns basic scheduling/status info for newsletters (admin only)
// @Summary Newsletter status
// @Description Returns subscriber stats and next approximate run time based on interval
// @Tags admin
// @Security Bearer
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/newsletter/status [get]
// 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 int64
var active int64
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 )
}
}
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
}
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 ,
} )
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 } )
}
// PreviewNewsletter builds a digest preview (subject + html) for admin without sending
// @Summary Preview newsletter digest (admin)
// @Description Returns subject and HTML for a digest newsletter using current cache and optional preferences
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @Param prefs body map[string]interface{} false "Optional { preferences: { blogs, matches, events, scores, competitions } }"
// @Success 200 {object} map[string]string
// @Failure 401 {object} map[string]string
// @Failure 403 {object} map[string]string
// @Router /api/v1/admin/newsletter/preview [post]
// 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" `
}
var input struct { Preferences map [ string ] interface { } ` json:"preferences" ` }
_ = c . ShouldBindJSON ( & input )
// Normalize preferences to NewsletterPrefs
prefs := services . NewsletterPrefs {
Email : "preview@local" ,
ContentTypes : [ ] string { } ,
Competitions : [ ] string { } ,
Frequency : "daily" ,
}
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 [ "event s" ] . ( 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 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 [ "score s" ] . ( bool ) ; ok && b { prefs . ContentTypes = append ( prefs . ContentTypes , "scores" ) }
if cs , ok := m [ "competitions" ] . ( string ) ; ok && strings . TrimSpace ( cs ) != "" {
parts := strings . Split ( cs , "," )
for _ , p := range parts {
if v := strings . TrimSpace ( p ) ; v != "" {
prefs . Competitions = append ( prefs . Competitions , v )
}
}
for _ , p := range strings . Split ( cs , "," ) { if v := strings . TrimSpace ( p ) ; v != "" { prefs . Competitions = append ( prefs . Competitions , v ) } }
}
}
cacheDir := "cache/prefetch"
subj , html := services . BuildNewsletterDigest ( cacheDir , prefs )
subj , html := services . BuildNewsletterDigest ( "cache/prefetch" , prefs )
c . JSON ( http . StatusOK , gin . H { "subject" : subj , "html" : html } )
}
// GetNewsletterPreferencesByToken returns subscriber preferences using a token (no auth required)
// GET /api/v1/newsletter/preferences?token=...
// 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
}
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
}
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 ,
} )
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 } )
}
// SaveNewsletterPreferencesByToken saves subscriber preferences using a token (no auth required)
// POST /api/v1/newsletter/preferences { token, 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
}
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
}
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
}
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 ) {
@@ -343,13 +267,7 @@ func (cc *ContactController) SaveNewsletterPreferencesByToken(c *gin.Context) {
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 )
}
}
}
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
@@ -360,7 +278,6 @@ func (cc *ContactController) SaveNewsletterPreferencesByToken(c *gin.Context) {
return
}
}
if compVal , ok := jm [ "competitions" ] ; ok {
if compStr , ok := compVal . ( string ) ; ok {
comp := strings . TrimSpace ( compStr )
@@ -374,32 +291,18 @@ func (cc *ContactController) SaveNewsletterPreferencesByToken(c *gin.Context) {
}
}
}
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
}
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" } )
}
// UnsubscribeByToken disables newsletter using a token (no auth required)
// POST /api/v1/newsletter/unsubscribe-token { token }
// 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
}
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 != 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
@@ -407,213 +310,67 @@ func (cc *ContactController) UnsubscribeByToken(c *gin.Context) {
c . JSON ( http . StatusOK , gin . H { "message" : "You have been unsubscribed" } )
}
// DeleteNewsletterSubscriber deletes a newsletter subscriber (admin only)
// @Summary Delete newsletter subscriber
// @Description Deletes a newsletter subscriber by ID (admin only)
// @Tags admin
// @Security Bearer
// @Produce json
// @Param id path int true "Subscriber 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/newsletter/subscribers/{id} [delete]
// 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
}
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
}
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
}
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" } )
}
// UpdateNewsletterSubscriberStatus toggles a subscriber's active status (admin only)
// @Summary Update newsletter subscriber status
// @Description Updates the is_active status of a newsletter subscriber (admin only)
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @Param id path int true "Subscriber ID"
// @Param input body map[string]bool true "{ is_active: boolean }"
// @Success 200 {object} models.NewsletterSubscription
// @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/newsletter/subscribers/{id}/status [patch]
// 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
}
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
}
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" } )
}
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
}
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 )
}
// UpdateN ewsletterS ubscriberPreferences updates subscriber preferences (admin only)
// @Summary Update newsletter subscriber preferences
// @Description Updates the preferences JSON for a subscriber (admin only)
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @Param id path int true "Subscriber ID"
// @Param input body map[string]bool true "Preferences map"
// @Success 200 {object} models.NewsletterSubscription
// @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/newsletter/subscribers/{id}/preferences [patch]
// PATCH /api/v1/admin/n ewsletter/s ubscribers/:id/preferences
func ( cc * ContactController ) UpdateNewsletterSubscriberPreferences ( c * gin . Context ) {
if c . GetString ( "userRole" ) != "admin" {
c . JSON ( http . StatusForbidden , gin . H { "error" : "Access denied" } )
return
}
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
}
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
}
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" } )
}
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
}
// convert map[string]bool to datatypes.JSONMap
jm := datatypes . JSONMap { }
for k , v := range prefs {
jm [ k ] = v
}
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
}
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 )
}
// SendNewsletterTest sends a test newsletter email to a single recipient (admin only)
// @Summary Send test newsletter email
// @Description Sends a test newsletter email to a single recipient (admin only)
// @Tags admin
// @Security Bearer
// @Accept json
// @Produce json
// @Param input body map[string]string false "Optional {email} to send test to"
// @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/test [post]
// 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" `
}
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 )
// Resolve recipients (emails > email > admin)
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"
}
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> `
@@ -621,155 +378,34 @@ func (cc *ContactController) SendNewsletterTest(c *gin.Context) {
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" } )
}
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 {
w := & email . NewsletterWelcomeData { Email : r , UnsubscribeLink : "" }
if err := cc . emailService . SendNewsletterWelcome ( w ) ; err != nil {
logger . Error ( "Failed to send welcome test to %s: %v" , r , err )
}
}
for _ , r := range recipients { _ = cc . emailService . SendNewsletterWelcome ( & email . NewsletterWelcomeData { Email : r } ) }
case "welcome_back" :
for _ , r := range recipients {
w := & email . NewsletterWelcomeBackData { Email : r }
if err := cc . emailService . SendNewsletterWelcomeBack ( w ) ; err != nil {
logger . Error ( "Failed to send welcome back test to %s: %v" , r , err )
}
}
for _ , r := range recipients { _ = cc . emailService . SendNewsletterWelcomeBack ( & email . NewsletterWelcomeBackData { Email : r } ) }
case "setup" :
// Test subscription setup email with token
for _ , r := range recipients {
token , tErr := utils . GenerateSubscriberToken ( r , 60 * 24 )
if tErr != nil {
logger . Error ( "Failed to generate token for setup test: %v" , tErr )
continue
}
if tErr != nil { logger . Error ( "Failed to generate token for setup test: %v" , tErr ) ; continue }
baseFE := strings . TrimSuffix ( config . AppConfig . FrontendBaseURL , "/" )
setupURL := baseFE + "/newsletter/setup?token=" + url . QueryEscape ( token )
setupEmail := & email . EmailData {
Subject : "Test: Nastavte svůj newsletter" ,
To : [ ] string { r } ,
Template : "newsletter_setup" ,
Data : struct { SetupURL string } { SetupURL : setupURL } ,
}
if err := cc . emailService . SendEmail ( setupEmail ) ; err != nil {
logger . Error ( "Failed to send setup test to %s: %v" , r , err )
}
setupEmail := & email . EmailData { Subject : "Test: Nastavte svůj newsletter" , To : [ ] string { r } , Template : "newsletter_setup" , Data : struct { SetupURL string } { SetupURL : setupURL } }
_ = cc . emailService . SendEmail ( setupEmail )
}
case "match_reminder_48h" :
// Test 48h match reminder
testHTML := `
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<h2 style="color: #1e3a8a; margin-bottom: 20px;">Připomínáme nadcházející zápas:</h2>
<div style="border-left: 4px solid #38a169; padding: 20px; background: #f0fff4; margin: 20px 0;">
<h3 style="margin: 0 0 10px 0; color: #1e3a8a; font-size: 24px;">FC Test vs SK Example</h3>
<p style="color: #276749; margin: 5px 0;"><strong>Datum:</strong> 2025-10-02</p>
<p style="color: #276749; margin: 5px 0;"><strong>Čas:</strong> 17:00</p>
<p style="color: #276749; margin: 5px 0;"><strong>Soutěž:</strong> MFS A</p>
<p style="color: #276749; margin: 5px 0;"><strong>Místo:</strong> Sportovní areál Test</p>
</div>
<p style="color: #4a5568; margin-top: 20px;">Zápas začíná za 48 hodin. Nezapomeňte!</p>
</div> `
data := & email . NewsletterData { Subject : "Test: Nadcházející zápas za 48 hodin" , Content : testHTML , Recipients : recipients }
if err := cc . emailService . SendNewsletter ( data ) ; err != nil {
logger . Error ( "Failed to send match reminder test: %v" , err )
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "Failed to send test" } )
return
}
case "match_reminder_today" :
// Test day-of match reminder
testHTML := `
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<h2 style="color: #1e3a8a; margin-bottom: 20px;">Zápas je dnes!</h2>
<div style="border-left: 4px solid #38a169; padding: 20px; background: #f0fff4; margin: 20px 0;">
<h3 style="margin: 0 0 10px 0; color: #1e3a8a; font-size: 24px;">FC Test vs SK Example</h3>
<p style="color: #276749; margin: 5px 0;"><strong>Datum:</strong> Dnes</p>
<p style="color: #276749; margin: 5px 0;"><strong>Čas:</strong> 17:00</p>
<p style="color: #276749; margin: 5px 0;"><strong>Soutěž:</strong> MFS A</p>
<p style="color: #276749; margin: 5px 0;"><strong>Místo:</strong> Sportovní areál Test</p>
</div>
<p style="color: #4a5568; margin-top: 20px;">Přijďte fandit!</p>
</div> `
data := & email . NewsletterData { Subject : "Test: Zápas dnes" , Content : testHTML , Recipients : recipients }
if err := cc . emailService . SendNewsletter ( data ) ; err != nil {
logger . Error ( "Failed to send match today test: %v" , err )
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "Failed to send test" } )
return
}
case "blog_notification" :
// Test blog release notification
testHTML := `
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<h2 style="color: #1e3a8a; margin-bottom: 20px;">Nový článek na webu</h2>
<div style="border-left: 4px solid #2563eb; padding: 20px; background: #f8fafc; margin: 20px 0;">
<h3 style="margin: 0 0 15px 0; color: #1e3a8a;">Testovací článek: Zajímavosti ze sezóny</h3>
<p style="color: #4a5568; line-height: 1.6; margin: 0 0 15px 0;">Toto je ukázkový výňatek z nového článku na našem webu. Přečtěte si celý příběh a dozvíte se více zajímavostí ze sezóny.</p>
<a href="https://example.com/news/test" style="display: inline-block; padding: 12px 24px; background: #2563eb; color: white; text-decoration: none; border-radius: 6px; font-weight: 600;">Číst článek</a>
</div>
</div> `
data := & email . NewsletterData { Subject : "Test: Nový článek - Zajímavosti ze sezóny" , Content : testHTML , Recipients : recipients }
if err := cc . emailService . SendNewsletter ( data ) ; err != nil {
logger . Error ( "Failed to send blog notification test: %v" , err )
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "Failed to send test" } )
return
}
case "match_result" :
// Test match result notification
testHTML := `
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<h2 style="color: #1e3a8a; margin-bottom: 20px;">Výsledek zápasu</h2>
<div style="border-left: 4px solid #d69e2e; padding: 20px; background: #fffbeb; margin: 20px 0;">
<h3 style="margin: 0 0 10px 0; color: #1e3a8a; font-size: 24px;">FC Test <span style="color: #d69e2e;">3:2</span> SK Example</h3>
<p style="color: #975a16; margin: 5px 0;"><strong>Datum:</strong> 2025-09-30</p>
<p style="color: #975a16; margin: 5px 0;"><strong>Soutěž:</strong> MFS A</p>
<p style="color: #975a16; margin: 10px 0 0 0;">Gratulujeme týmu k vítězství!</p>
</div>
</div> `
data := & email . NewsletterData { Subject : "Test: Výsledek - FC Test 3:2 SK Example" , Content : testHTML , Recipients : recipients }
if err := cc . emailService . SendNewsletter ( data ) ; err != nil {
logger . Error ( "Failed to send match result test: %v" , err )
c . JSON ( http . StatusInternalServerError , gin . H { "error" : "Failed to send test" } )
return
}
// Predefined digest test types mapped to content sections
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 }
}
cacheDir := "cache/prefetch"
subj , html := services . BuildNewsletterDigest ( cacheDir , prefs )
if subj == "" {
subj = "Test digest"
}
if html == "" {
html = "<p>Momentálně žádný obsah pro zvolený typ.</p>"
}
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
}
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 } )
}
@@ -928,7 +564,7 @@ func (cc *ContactController) SubscribeToNewsletter(c *gin.Context) {
// 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 }
prefs = map [ string ] bool { "weekly" : true , "matches" : true , "blogs" : true , "events" : true , "scores" : true }
}
// convert to datatypes.JSONMap
jm := datatypes . JSONMap { }