mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
dev day #75
This commit is contained in:
@@ -2266,8 +2266,6 @@ func (bc *BaseController) PatchTeamLogoOverride(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
// ProxyImage streams a remote image to the client to avoid browser CORS restrictions for Canvas operations
|
||||
// GET /api/v1/proxy/image?url=<remote_image_url>
|
||||
func (bc *BaseController) ProxyImage(c *gin.Context) {
|
||||
raw := c.Query("url")
|
||||
if raw == "" {
|
||||
@@ -2287,8 +2285,15 @@ func (bc *BaseController) ProxyImage(c *gin.Context) {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "request init failed"})
|
||||
return
|
||||
}
|
||||
// Some CDNs require a UA
|
||||
req.Header.Set("User-Agent", "fotbal-club/1.0 (+https://localhost)")
|
||||
// Use realistic browser headers - some CDNs block unknown clients
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0 Safari/537.36")
|
||||
req.Header.Set("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8")
|
||||
req.Header.Set("Accept-Language", "cs-CZ,cs;q=0.9,en;q=0.8")
|
||||
// Set a benign referer tied to the target host to satisfy anti-hotlink checks
|
||||
if u.Host != "" {
|
||||
ref := u.Scheme + "://" + u.Host + "/"
|
||||
req.Header.Set("Referer", ref)
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": "fetch failed"})
|
||||
@@ -2366,6 +2371,10 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
ClubLogoURL string `json:"club_logo_url"`
|
||||
ClubURL string `json:"club_url"`
|
||||
|
||||
// Optional bases
|
||||
FrontendBaseURL string `json:"frontend_base_url"`
|
||||
APIBaseURL string `json:"api_base_url"`
|
||||
|
||||
// Social profiles (optional)
|
||||
FacebookURL string `json:"facebook_url"`
|
||||
InstagramURL string `json:"instagram_url"`
|
||||
@@ -2473,6 +2482,45 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
if body.FrontpageStyle != "" {
|
||||
s.FrontpageStyle = body.FrontpageStyle
|
||||
}
|
||||
|
||||
// Detect and persist base URLs
|
||||
if v := strings.TrimSpace(body.APIBaseURL); v != "" {
|
||||
s.APIBaseURL = v
|
||||
}
|
||||
if v := strings.TrimSpace(body.FrontendBaseURL); v != "" {
|
||||
s.FrontendBaseURL = v
|
||||
}
|
||||
// If not provided, infer from current request and proxy headers
|
||||
{
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil || strings.EqualFold(c.Request.Header.Get("X-Forwarded-Proto"), "https") || strings.Contains(strings.ToLower(c.Request.Header.Get("CF-Visitor")), "https") {
|
||||
scheme = "https"
|
||||
}
|
||||
host := c.Request.Host
|
||||
if xf := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Host")); xf != "" {
|
||||
parts := strings.Split(xf, ",")
|
||||
if len(parts) > 0 {
|
||||
if h := strings.TrimSpace(parts[0]); h != "" { host = h }
|
||||
}
|
||||
}
|
||||
if !strings.Contains(host, ":") {
|
||||
if xfp := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Port")); xfp != "" {
|
||||
if (scheme == "http" && xfp != "80") || (scheme == "https" && xfp != "443") {
|
||||
host = host + ":" + xfp
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(s.APIBaseURL) == "" {
|
||||
s.APIBaseURL = scheme + "://" + host + "/api/v1"
|
||||
}
|
||||
if strings.TrimSpace(s.FrontendBaseURL) == "" {
|
||||
if origin := strings.TrimSpace(c.Request.Header.Get("Origin")); origin != "" {
|
||||
s.FrontendBaseURL = origin
|
||||
} else {
|
||||
s.FrontendBaseURL = scheme + "://" + host
|
||||
}
|
||||
}
|
||||
}
|
||||
// SMTP overrides from initial setup
|
||||
if body.SMTP != nil {
|
||||
if v := strings.TrimSpace(body.SMTP.Host); v != "" {
|
||||
@@ -2594,7 +2642,11 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
_ = os.WriteFile(tmp, b, 0o644)
|
||||
_ = os.Rename(tmp, outPath)
|
||||
}(s)
|
||||
go services.PrefetchOnce(getPrefetchBaseURL())
|
||||
{
|
||||
base := strings.TrimSpace(s.APIBaseURL)
|
||||
if base == "" { base = getPrefetchBaseURL() }
|
||||
go services.PrefetchOnce(strings.TrimRight(base, "/"))
|
||||
}
|
||||
if strings.TrimSpace(s.YoutubeURL) != "" {
|
||||
go func(u string) { _ = services.RefreshYouTubeChannelNow(u) }(s.YoutubeURL)
|
||||
}
|
||||
@@ -2603,7 +2655,7 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
go func(link string) { _ = services.RefreshZoneramaNow(link) }(g)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Inicializace již byla provedena"})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Inicializace již byla provedena", "frontend_base_url": s.FrontendBaseURL, "api_base_url": s.APIBaseURL})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -2666,6 +2718,10 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
From string `json:"from"`
|
||||
UseTLS *bool `json:"use_tls"`
|
||||
} `json:"smtp"`
|
||||
|
||||
// Optional bases
|
||||
FrontendBaseURL string `json:"frontend_base_url"`
|
||||
APIBaseURL string `json:"api_base_url"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
@@ -2806,6 +2862,44 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
if body.FrontpageStyle != "" {
|
||||
s.FrontpageStyle = body.FrontpageStyle
|
||||
}
|
||||
|
||||
// Persist base URLs (prefer request body, otherwise infer)
|
||||
if v := strings.TrimSpace(body.APIBaseURL); v != "" {
|
||||
s.APIBaseURL = v
|
||||
}
|
||||
if v := strings.TrimSpace(body.FrontendBaseURL); v != "" {
|
||||
s.FrontendBaseURL = v
|
||||
}
|
||||
{
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil || strings.EqualFold(c.Request.Header.Get("X-Forwarded-Proto"), "https") || strings.Contains(strings.ToLower(c.Request.Header.Get("CF-Visitor")), "https") {
|
||||
scheme = "https"
|
||||
}
|
||||
host := c.Request.Host
|
||||
if xf := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Host")); xf != "" {
|
||||
parts := strings.Split(xf, ",")
|
||||
if len(parts) > 0 {
|
||||
if h := strings.TrimSpace(parts[0]); h != "" { host = h }
|
||||
}
|
||||
}
|
||||
if !strings.Contains(host, ":") {
|
||||
if xfp := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Port")); xfp != "" {
|
||||
if (scheme == "http" && xfp != "80") || (scheme == "https" && xfp != "443") {
|
||||
host = host + ":" + xfp
|
||||
}
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(s.APIBaseURL) == "" {
|
||||
s.APIBaseURL = scheme + "://" + host + "/api/v1"
|
||||
}
|
||||
if strings.TrimSpace(s.FrontendBaseURL) == "" {
|
||||
if origin := strings.TrimSpace(c.Request.Header.Get("Origin")); origin != "" {
|
||||
s.FrontendBaseURL = origin
|
||||
} else {
|
||||
s.FrontendBaseURL = scheme + "://" + host
|
||||
}
|
||||
}
|
||||
}
|
||||
// SMTP overrides from initial setup
|
||||
if body.SMTP != nil {
|
||||
if v := strings.TrimSpace(body.SMTP.Host); v != "" {
|
||||
@@ -2940,12 +3034,13 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
logger.Info("Starting initial data prefetch and setup operations in background...")
|
||||
|
||||
// Run all setup operations in a single background goroutine
|
||||
go func(settingsID uint, youtubeURL, galleryURL, adminEmail string) {
|
||||
go func(settingsID uint, youtubeURL, galleryURL, adminEmail string, apiBase string) {
|
||||
defer func() { _ = recover() }()
|
||||
|
||||
// 1. Trigger prefetch (matches, standings, etc.)
|
||||
baseURL := getPrefetchBaseURL()
|
||||
services.PrefetchOnce(baseURL)
|
||||
baseURL := strings.TrimSpace(apiBase)
|
||||
if baseURL == "" { baseURL = getPrefetchBaseURL() }
|
||||
services.PrefetchOnce(strings.TrimRight(baseURL, "/"))
|
||||
logger.Info("Background prefetch completed")
|
||||
|
||||
// Auto-populate competition aliases from FACR data
|
||||
@@ -2984,10 +3079,10 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) {
|
||||
}
|
||||
|
||||
logger.Info("All background setup operations completed")
|
||||
}(s.ID, s.YoutubeURL, s.GalleryURL, admin.Email)
|
||||
}(s.ID, s.YoutubeURL, s.GalleryURL, admin.Email, s.APIBaseURL)
|
||||
|
||||
logger.Info("SetupInitialize finished successfully - background operations running")
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Setup completed successfully"})
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Setup completed successfully", "frontend_base_url": s.FrontendBaseURL, "api_base_url": s.APIBaseURL})
|
||||
}
|
||||
|
||||
// UpdateSettings updates settings (upsert singleton)
|
||||
@@ -3103,6 +3198,10 @@ func (bc *BaseController) UpdateSettings(c *gin.Context) {
|
||||
|
||||
// Homepage matches display configuration
|
||||
FinishedMatchDisplayDays *int `json:"finished_match_display_days"`
|
||||
|
||||
// Deployment base URLs (optional, for domain/IP change)
|
||||
FrontendBaseURL *string `json:"frontend_base_url"`
|
||||
APIBaseURL *string `json:"api_base_url"`
|
||||
}
|
||||
var body reqBody
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
@@ -3404,6 +3503,14 @@ func (bc *BaseController) UpdateSettings(c *gin.Context) {
|
||||
s.FinishedMatchDisplayDays = *body.FinishedMatchDisplayDays
|
||||
}
|
||||
|
||||
// Deployment base URLs
|
||||
if body.FrontendBaseURL != nil {
|
||||
s.FrontendBaseURL = strings.TrimSpace(*body.FrontendBaseURL)
|
||||
}
|
||||
if body.APIBaseURL != nil {
|
||||
s.APIBaseURL = strings.TrimSpace(*body.APIBaseURL)
|
||||
}
|
||||
|
||||
if s.ID == 0 {
|
||||
if err := bc.DB.Create(&s).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze uložit nastavení"})
|
||||
@@ -3425,10 +3532,11 @@ func (bc *BaseController) UpdateSettings(c *gin.Context) {
|
||||
}
|
||||
logger.Info("UpdateSettings saved: club_id=%s club_name=%s gallery_url=%s gallery_label=%s", s.ClubID, s.ClubName, s.GalleryURL, s.GalleryLabel)
|
||||
// Best-effort: trigger prefetch so cached settings.json and dependent files update immediately
|
||||
go func() {
|
||||
base := getPrefetchBaseURL()
|
||||
services.PrefetchOnce(base)
|
||||
}()
|
||||
go func(urlFromSettings string) {
|
||||
base := strings.TrimSpace(urlFromSettings)
|
||||
if base == "" { base = getPrefetchBaseURL() }
|
||||
services.PrefetchOnce(strings.TrimRight(base, "/"))
|
||||
}(s.APIBaseURL)
|
||||
// If gallery_url is a Zonerama link, refresh Zonerama cache immediately
|
||||
if g := strings.TrimSpace(s.GalleryURL); g != "" && strings.Contains(strings.ToLower(g), "zonerama.com") {
|
||||
go func(link string) { _ = services.RefreshZoneramaNow(link) }(g)
|
||||
@@ -3569,6 +3677,9 @@ func (bc *BaseController) GetPublicSettings(c *gin.Context) {
|
||||
"map_zoom_level": s.MapZoomLevel,
|
||||
"map_style": s.MapStyle,
|
||||
"show_map_on_homepage": s.ShowMapOnHomepage,
|
||||
// Deployment base URLs (hints for frontend tooling)
|
||||
"frontend_base_url": s.FrontendBaseURL,
|
||||
"api_base_url": s.APIBaseURL,
|
||||
}
|
||||
logger.Debug("GetPublicSettings response includes gallery: url=%s label=%s", s.GalleryURL, s.GalleryLabel)
|
||||
c.JSON(http.StatusOK, resp)
|
||||
@@ -4087,6 +4198,127 @@ func (bc *BaseController) DeleteSponsor(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"zprava": "Sponzor byl smazán"})
|
||||
}
|
||||
|
||||
// Banners (separate from sponsors)
|
||||
func (bc *BaseController) GetBanners(c *gin.Context) {
|
||||
var items []models.Banner
|
||||
q := bc.DB.Model(&models.Banner{})
|
||||
activeOnly := strings.ToLower(strings.TrimSpace(c.DefaultQuery("active", "true"))) != "false"
|
||||
if activeOnly {
|
||||
q = q.Where("is_active = ?", true)
|
||||
}
|
||||
if p := strings.TrimSpace(c.Query("placement")); p != "" {
|
||||
q = q.Where("placement = ?", p)
|
||||
}
|
||||
if err := q.Order("display_order ASC, created_at ASC").Find(&items).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Chyba databáze"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, items)
|
||||
}
|
||||
|
||||
func (bc *BaseController) CreateBanner(c *gin.Context) {
|
||||
var body struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
ImageURL string `json:"image_url"`
|
||||
ClickURL string `json:"click_url"`
|
||||
Placement string `json:"placement"`
|
||||
Width *int `json:"width"`
|
||||
Height *int `json:"height"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
DisplayOrder *int `json:"display_order"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Neplatná data", "detail": err.Error()})
|
||||
return
|
||||
}
|
||||
name := strings.TrimSpace(body.Name)
|
||||
if name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Název banneru je povinný"})
|
||||
return
|
||||
}
|
||||
item := models.Banner{
|
||||
Name: name,
|
||||
ImageURL: strings.TrimSpace(body.ImageURL),
|
||||
ClickURL: strings.TrimSpace(body.ClickURL),
|
||||
Placement: strings.TrimSpace(body.Placement),
|
||||
IsActive: true,
|
||||
}
|
||||
if body.Width != nil { item.Width = *body.Width }
|
||||
if body.Height != nil { item.Height = *body.Height }
|
||||
if body.DisplayOrder != nil { item.DisplayOrder = *body.DisplayOrder }
|
||||
if body.IsActive != nil { item.IsActive = *body.IsActive }
|
||||
if err := bc.DB.Create(&item).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze vytvořit banner"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, item)
|
||||
}
|
||||
|
||||
func (bc *BaseController) UpdateBanner(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var item models.Banner
|
||||
if err := bc.DB.First(&item, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"chyba": "Banner nenalezen"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Chyba databáze"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
Name *string `json:"name"`
|
||||
ImageURL *string `json:"image_url"`
|
||||
ClickURL *string `json:"click_url"`
|
||||
Placement *string `json:"placement"`
|
||||
Width *int `json:"width"`
|
||||
Height *int `json:"height"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
DisplayOrder *int `json:"display_order"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Neplatná data", "detail": err.Error()})
|
||||
return
|
||||
}
|
||||
if body.Name != nil {
|
||||
v := strings.TrimSpace(*body.Name)
|
||||
if v == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"chyba": "Název banneru nemůže být prázdný"})
|
||||
return
|
||||
}
|
||||
item.Name = v
|
||||
}
|
||||
if body.ImageURL != nil { item.ImageURL = strings.TrimSpace(*body.ImageURL) }
|
||||
if body.ClickURL != nil { item.ClickURL = strings.TrimSpace(*body.ClickURL) }
|
||||
if body.Placement != nil { item.Placement = strings.TrimSpace(*body.Placement) }
|
||||
if body.Width != nil { item.Width = *body.Width }
|
||||
if body.Height != nil { item.Height = *body.Height }
|
||||
if body.IsActive != nil { item.IsActive = *body.IsActive }
|
||||
if body.DisplayOrder != nil { item.DisplayOrder = *body.DisplayOrder }
|
||||
if err := bc.DB.Save(&item).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze aktualizovat banner"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, item)
|
||||
}
|
||||
|
||||
func (bc *BaseController) DeleteBanner(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
var item models.Banner
|
||||
if err := bc.DB.First(&item, id).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"chyba": "Banner nenalezen"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Chyba databáze"})
|
||||
return
|
||||
}
|
||||
if err := bc.DB.Delete(&item).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"chyba": "Nelze smazat banner"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"zprava": "Banner byl smazán"})
|
||||
}
|
||||
|
||||
func (bc *BaseController) UploadImage(c *gin.Context) {
|
||||
f, err := c.FormFile("file")
|
||||
if err != nil {
|
||||
@@ -4150,15 +4382,37 @@ func (bc *BaseController) UploadImage(c *gin.Context) {
|
||||
|
||||
// Build absolute URL from request (supports proxies)
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil || strings.EqualFold(c.Request.Header.Get("X-Forwarded-Proto"), "https") {
|
||||
if c.Request.TLS != nil || strings.EqualFold(c.Request.Header.Get("X-Forwarded-Proto"), "https") || strings.Contains(strings.ToLower(c.Request.Header.Get("CF-Visitor")), "https") {
|
||||
scheme = "https"
|
||||
}
|
||||
host := c.Request.Host
|
||||
if xf := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Host")); xf != "" {
|
||||
host = xf
|
||||
// Take the first value if comma-separated
|
||||
parts := strings.Split(xf, ",")
|
||||
if len(parts) > 0 {
|
||||
h := strings.TrimSpace(parts[0])
|
||||
if h != "" { host = h }
|
||||
}
|
||||
}
|
||||
// Append forwarded port when host has no explicit port and it's non-default
|
||||
if !strings.Contains(host, ":") {
|
||||
if xfp := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Port")); xfp != "" {
|
||||
if (scheme == "http" && xfp != "80") || (scheme == "https" && xfp != "443") {
|
||||
host = host + ":" + xfp
|
||||
}
|
||||
}
|
||||
}
|
||||
absolute := scheme + "://" + host + urlPath
|
||||
c.JSON(http.StatusOK, gin.H{"url": absolute})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
// Always return a backend-relative path for storage
|
||||
"url": urlPath,
|
||||
// Convenience absolute URL for immediate usage in UIs
|
||||
"absolute_url": absolute,
|
||||
// Basic metadata (best-effort)
|
||||
"name": outName,
|
||||
"type": mimeType,
|
||||
"size": f.Size,
|
||||
})
|
||||
}
|
||||
|
||||
// Global newsletter automation instance (set from main)
|
||||
|
||||
@@ -1099,15 +1099,15 @@ func (cc *ContactController) ForwardContactMessage(c *gin.Context) {
|
||||
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"})
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Prepare email data for forwarding
|
||||
// Prepare email data for forwarding (Czech subject)
|
||||
forwardData := &email.EmailData{
|
||||
Subject: fmt.Sprintf("Fwd: Contact Form - %s", message.Subject),
|
||||
To: []string{input.ToEmail},
|
||||
Subject: fmt.Sprintf("Přeposláno: Kontaktní formulář - %s", message.Subject),
|
||||
To: []string{input.ToEmail},
|
||||
Template: "contact_form",
|
||||
Data: struct {
|
||||
Name string
|
||||
@@ -1128,26 +1128,21 @@ func (cc *ContactController) ForwardContactMessage(c *gin.Context) {
|
||||
},
|
||||
}
|
||||
|
||||
// Send email asynchronously
|
||||
go func() {
|
||||
if err := cc.emailService.SendEmail(forwardData); err != nil {
|
||||
logger.Error("Failed to forward contact message %d to %s: %v", id, input.ToEmail, err)
|
||||
} else {
|
||||
logger.Info("Contact message %d forwarded to %s", id, input.ToEmail)
|
||||
}
|
||||
}()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Message is being forwarded to " + input.ToEmail})
|
||||
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"})
|
||||
}
|
||||
|
||||
// ForwardAllContactMessages forwards all contact messages to a specified email (admin only)
|
||||
// @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 }"
|
||||
// @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
|
||||
@@ -1160,13 +1155,76 @@ func (cc *ContactController) ForwardAllContactMessages(c *gin.Context) {
|
||||
}
|
||||
|
||||
var input struct {
|
||||
ToEmail string `json:"to_email" binding:"required,email"`
|
||||
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": "Valid email address is required"})
|
||||
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 {
|
||||
@@ -1180,12 +1238,12 @@ func (cc *ContactController) ForwardAllContactMessages(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Forward all messages asynchronously
|
||||
go func(msgs []models.ContactMessage, toEmail string) {
|
||||
go func(msgs []models.ContactMessage, dest []string) {
|
||||
successCount := 0
|
||||
for _, message := range msgs {
|
||||
forwardData := &email.EmailData{
|
||||
Subject: fmt.Sprintf("Fwd: Contact Form - %s", message.Subject),
|
||||
To: []string{toEmail},
|
||||
Subject: fmt.Sprintf("Přeposláno: Kontaktní formulář - %s", message.Subject),
|
||||
To: dest,
|
||||
Template: "contact_form",
|
||||
Data: struct {
|
||||
Name string
|
||||
@@ -1207,16 +1265,16 @@ func (cc *ContactController) ForwardAllContactMessages(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := cc.emailService.SendEmail(forwardData); err != nil {
|
||||
logger.Error("Failed to forward contact message %d to %s: %v", message.ID, toEmail, err)
|
||||
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 %s", successCount, len(msgs), toEmail)
|
||||
}(messages, input.ToEmail)
|
||||
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("Forwarding %d message(s) to %s", len(messages), input.ToEmail),
|
||||
"message": fmt.Sprintf("Přeposílám %d zpráv na: %s", len(messages), strings.Join(recipients, ", ")),
|
||||
"count": len(messages),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -159,8 +159,9 @@ func (ctrl *ImageProcessingController) ProcessImage(c *gin.Context) {
|
||||
}
|
||||
absolute := scheme + "://" + host + outputPath
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"url": absolute,
|
||||
"format": format,
|
||||
"url": outputPath,
|
||||
"absolute_url": absolute,
|
||||
"format": format,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -346,7 +347,8 @@ func (ctrl *ImageProcessingController) CropAndUpload(c *gin.Context) {
|
||||
}
|
||||
absolute := scheme + "://" + host + outputPath
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"url": absolute,
|
||||
"url": outputPath,
|
||||
"absolute_url": absolute,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -431,7 +433,28 @@ func (ctrl *ImageProcessingController) QuickEdit(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
scheme := "http"
|
||||
if c.Request.TLS != nil || strings.EqualFold(c.Request.Header.Get("X-Forwarded-Proto"), "https") {
|
||||
scheme = "https"
|
||||
}
|
||||
host := c.Request.Host
|
||||
if xf := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Host")); xf != "" {
|
||||
parts := strings.Split(xf, ",")
|
||||
if len(parts) > 0 {
|
||||
h := strings.TrimSpace(parts[0])
|
||||
if h != "" { host = h }
|
||||
}
|
||||
}
|
||||
if !strings.Contains(host, ":") {
|
||||
if xfp := strings.TrimSpace(c.Request.Header.Get("X-Forwarded-Port")); xfp != "" {
|
||||
if (scheme == "http" && xfp != "80") || (scheme == "https" && xfp != "443") {
|
||||
host = host + ":" + xfp
|
||||
}
|
||||
}
|
||||
}
|
||||
absolute := scheme + "://" + host + outputPath
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"url": outputPath,
|
||||
"url": outputPath,
|
||||
"absolute_url": absolute,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ func SecurityHeaders() gin.HandlerFunc {
|
||||
c.Header("X-Permitted-Cross-Domain-Policies", "none")
|
||||
c.Header("Cross-Origin-Embedder-Policy", "require-corp")
|
||||
c.Header("Cross-Origin-Opener-Policy", "same-origin")
|
||||
c.Header("Cross-Origin-Resource-Policy", "same-origin")
|
||||
// Allow assets (e.g., /uploads) to be embedded from different origin (frontend vs backend)
|
||||
c.Header("Cross-Origin-Resource-Policy", "cross-origin")
|
||||
|
||||
c.Next()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
type Banner struct {
|
||||
gorm.Model
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
ImageURL string `json:"image_url"`
|
||||
ClickURL string `json:"click_url"`
|
||||
Placement string `json:"placement" gorm:"index"` // e.g., homepage_top, homepage_sidebar, homepage_under_table
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
IsActive bool `json:"is_active" gorm:"default:true;index"`
|
||||
DisplayOrder int `json:"display_order" gorm:"default:0;index"`
|
||||
}
|
||||
|
||||
func (Banner) TableName() string { return "banners" }
|
||||
@@ -187,6 +187,12 @@ type Settings struct {
|
||||
AdditionalMeta string `gorm:"type:text" json:"additional_meta"` // raw extra meta
|
||||
EnableIndexing bool `json:"enable_indexing"` // robots allow/disallow
|
||||
|
||||
// Deployment base URLs (optional runtime hints)
|
||||
// FrontendBaseURL: e.g. https://club.example.com
|
||||
FrontendBaseURL string `json:"frontend_base_url"`
|
||||
// APIBaseURL: full API root, e.g. https://api.example.com/api/v1 or https://backend.example.com/api/v1
|
||||
APIBaseURL string `json:"api_base_url"`
|
||||
|
||||
// Social profiles
|
||||
FacebookURL string `json:"facebook_url"`
|
||||
InstagramURL string `json:"instagram_url"`
|
||||
@@ -241,6 +247,9 @@ type Settings struct {
|
||||
ContactCountry string `json:"contact_country"`
|
||||
ContactPhone string `json:"contact_phone"`
|
||||
ContactEmail string `json:"contact_email"`
|
||||
// Contact form auto-forwarding
|
||||
ContactForwardEnabled bool `json:"contact_forward_enabled"`
|
||||
ContactForwardList string `gorm:"type:text" json:"contact_forward_list"` // comma/space/semicolon-separated emails
|
||||
LocationLatitude float64 `json:"location_latitude"`
|
||||
LocationLongitude float64 `json:"location_longitude"`
|
||||
MapZoomLevel int `gorm:"default:15" json:"map_zoom_level"`
|
||||
|
||||
@@ -199,6 +199,15 @@ func SetupRoutes(api *gin.RouterGroup, db *gorm.DB) {
|
||||
sponsors.DELETE("/:id", baseController.DeleteSponsor)
|
||||
}
|
||||
|
||||
// Banners (protected CRUD)
|
||||
banners := protected.Group("/banners")
|
||||
banners.Use(middleware.RoleAuth("admin"))
|
||||
{
|
||||
banners.POST("", baseController.CreateBanner)
|
||||
banners.PUT("/:id", baseController.UpdateBanner)
|
||||
banners.DELETE("/:id", baseController.DeleteBanner)
|
||||
}
|
||||
|
||||
// Admin routes (single consolidated group)
|
||||
admin := protected.Group("/admin")
|
||||
admin.Use(middleware.RoleAuth("admin"))
|
||||
@@ -488,6 +497,8 @@ func SetupRoutes(api *gin.RouterGroup, db *gorm.DB) {
|
||||
api.GET("/players", baseController.GetPlayers)
|
||||
api.GET("/players/:id", baseController.GetPlayer)
|
||||
api.GET("/sponsors", baseController.GetSponsors)
|
||||
// Public banners
|
||||
api.GET("/banners", baseController.GetBanners)
|
||||
api.GET("/matches", baseController.GetMatches)
|
||||
api.GET("/matches/history", baseController.GetMatchesHistory)
|
||||
api.GET("/standings", baseController.GetStandings)
|
||||
|
||||
Reference in New Issue
Block a user