mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32: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)
|
||||
|
||||
Reference in New Issue
Block a user