diff --git a/.env b/.env index b27ffb6..d52bec4 100644 --- a/.env +++ b/.env @@ -54,12 +54,12 @@ NEWSLETTER_ENABLED=true # File Uploads UPLOAD_DIR=./uploads -MAX_UPLOAD_SIZE=10485760 # 10MB +MAX_UPLOAD_SIZE=50 ALLOWED_FILE_TYPES=image/jpeg,image/png,image/gif,application/pdf MAX_FILES=5 # Maximum number of files per upload # CORS -ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080 +ALLOWED_ORIGINS=* # Frontend Configuration REACT_APP_NAME=Fotbal Club Manager REACT_APP_API_URL=/api/v1 diff --git a/frontend/src/components/gallery/PhotoModal.tsx b/frontend/src/components/gallery/PhotoModal.tsx index 340a146..6d3dff7 100644 --- a/frontend/src/components/gallery/PhotoModal.tsx +++ b/frontend/src/components/gallery/PhotoModal.tsx @@ -15,6 +15,7 @@ import { VStack, } from '@chakra-ui/react'; import { Download, ExternalLink } from 'lucide-react'; +import { API_URL } from '../../services/api'; interface PhotoModalProps { isOpen: boolean; @@ -34,8 +35,7 @@ const PhotoModal: React.FC = ({ const toast = useToast(); const getProxyUrl = (url: string) => { - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080'; - return `${apiUrl}/api/v1/gallery/proxy-image?url=${encodeURIComponent(url)}`; + return `${API_URL}/gallery/proxy-image?url=${encodeURIComponent(url)}`; }; diff --git a/frontend/src/components/widgets/MatchesWidget.tsx b/frontend/src/components/widgets/MatchesWidget.tsx index 079ddf5..686a496 100644 --- a/frontend/src/components/widgets/MatchesWidget.tsx +++ b/frontend/src/components/widgets/MatchesWidget.tsx @@ -46,7 +46,7 @@ export const MatchesWidget = () => { const resolveUrl = (path: string) => { try { if (/^https?:\/\//i.test(path)) return path; - const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const origin = new URL(API_URL, window.location.origin).origin; return origin + path; } catch { return path; @@ -108,7 +108,7 @@ export const MatchesWidget = () => { } const chosen = candidate || orig; if (typeof chosen === 'string' && chosen.startsWith('/')) { - const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const origin = new URL(API_URL, window.location.origin).origin; return origin + chosen; } return chosen || (assetUrl('/dist/img/logo-club-empty.svg') as string); @@ -120,7 +120,7 @@ export const MatchesWidget = () => { queryKey: ['upcomingMatchesCache'], queryFn: async () => { // Build absolute origin from API URL env (which may include /api/v1) - const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const origin = new URL(API_URL, window.location.origin).origin; const url = `${origin}/cache/prefetch/facr_club_info.json`; const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } }); diff --git a/frontend/src/config.ts b/frontend/src/config.ts index f303b47..3a76af7 100644 --- a/frontend/src/config.ts +++ b/frontend/src/config.ts @@ -1,5 +1,5 @@ // API configuration -export const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || 'http://localhost:8080'; +export const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || ''; // App configuration export const APP_NAME = 'Fotbal Club'; diff --git a/frontend/src/pages/ArticleDetailPage.tsx b/frontend/src/pages/ArticleDetailPage.tsx index 47fc91b..4ad050e 100644 --- a/frontend/src/pages/ArticleDetailPage.tsx +++ b/frontend/src/pages/ArticleDetailPage.tsx @@ -61,7 +61,7 @@ const ArticleDetailPage: React.FC = () => { queryKey: ['facr-cached-match', (matchLinkQuery.data as any)?.external_match_id], enabled: Boolean((matchLinkQuery.data as any)?.external_match_id), queryFn: async () => { - const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const origin = new URL(API_URL, window.location.origin).origin; const url = `${origin}/cache/prefetch/facr_club_info.json`; const res = await fetch(url, { cache: 'no-cache' }); if (!res.ok) return null as any; @@ -105,8 +105,7 @@ const ArticleDetailPage: React.FC = () => { } } - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const origin = new URL(apiUrl).origin; + const origin = new URL(API_URL, window.location.origin).origin; // Try to find album in both sources const [profileRes, albumsRes] = await Promise.allSettled([ diff --git a/frontend/src/pages/MatchesPage.tsx b/frontend/src/pages/MatchesPage.tsx index 982c5ce..c8935a1 100644 --- a/frontend/src/pages/MatchesPage.tsx +++ b/frontend/src/pages/MatchesPage.tsx @@ -8,6 +8,7 @@ import { assetUrl, sanitizeClubName } from '../utils/url'; import MatchModal from '../components/home/MatchModal'; import { useCountdown, useMultipleCountdowns } from '../hooks/useCountdown'; import '../styles/theme.css'; +import { API_URL } from '../services/api'; type MatchItem = { id: number | string; @@ -225,8 +226,7 @@ const MatchesPage: React.FC = () => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const b = new URL(base); + const b = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : undefined); const abs = new URL(path, `${b.protocol}//${b.host}`); return abs.toString(); } diff --git a/frontend/src/pages/TablesPage.tsx b/frontend/src/pages/TablesPage.tsx index 855c7f3..64f26d2 100644 --- a/frontend/src/pages/TablesPage.tsx +++ b/frontend/src/pages/TablesPage.tsx @@ -8,6 +8,7 @@ import ClubModal from '../components/home/ClubModal'; import { usePublicSettings } from '../hooks/usePublicSettings'; import { sortCategoriesWithOrder } from '../utils/categorySort'; import { TeamLogo } from '../components/common/TeamLogo'; +import { API_URL } from '../services/api'; type TableRow = { rank: string; @@ -34,10 +35,9 @@ const resolveBackendUrl = (path: string) => { try { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads')) { - const base = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const u = new URL(base); - u.pathname = path; - return u.toString(); + const u = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : undefined); + const abs = new URL(path, `${u.protocol}//${u.host}`); + return abs.toString(); } return path; } catch { diff --git a/frontend/src/pages/admin/SponsorsAdminPage.tsx b/frontend/src/pages/admin/SponsorsAdminPage.tsx index 21f0d95..75824cc 100644 --- a/frontend/src/pages/admin/SponsorsAdminPage.tsx +++ b/frontend/src/pages/admin/SponsorsAdminPage.tsx @@ -48,7 +48,7 @@ const SponsorsAdminPage: React.FC = () => { const normalizeImageUrl = (url?: string) => { if (!url || url === '') return '/logo192.png'; if (/^https?:\/\//i.test(url)) return url; - const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const origin = new URL(API_URL, window.location.origin).origin; if (url.startsWith('/uploads/')) return `${origin}${url}`; return `${origin}${url.startsWith('/') ? '' : '/'}${url}`; }; @@ -129,8 +129,7 @@ const SponsorsAdminPage: React.FC = () => { if (!file) return; try { const res = await uploadFile(file); - const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; - const apiOrigin = new URL(apiUrl).origin; + const apiOrigin = new URL(API_URL, window.location.origin).origin; // If backend returned an absolute URL pointing to the dev host (same origin as app), rewrite to API origin let url = res.url || ''; try { diff --git a/frontend/src/pages/admin/StandingsAdminPage.tsx b/frontend/src/pages/admin/StandingsAdminPage.tsx index ec13a51..fab32c1 100644 --- a/frontend/src/pages/admin/StandingsAdminPage.tsx +++ b/frontend/src/pages/admin/StandingsAdminPage.tsx @@ -23,7 +23,7 @@ const StandingsAdminPage: React.FC = () => { const { data, isLoading, error } = useQuery({ queryKey: ['facr-tables-cache'], queryFn: async () => { - const origin = new URL(API_URL, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin; + const origin = new URL(API_URL, window.location.origin).origin; const url = `${origin}/cache/prefetch/facr_tables.json`; const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } }); if (!res.ok) throw new Error(`Failed to load cache: ${res.status}`); diff --git a/frontend/src/services/facr/facrApi.ts b/frontend/src/services/facr/facrApi.ts index fb197f9..22a2d75 100644 --- a/frontend/src/services/facr/facrApi.ts +++ b/frontend/src/services/facr/facrApi.ts @@ -31,11 +31,15 @@ const resolveBackendUrl = (path: string) => { if (/^https?:\/\//i.test(path)) return path; if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) { const explicit = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || ''; - const origin = explicit - ? new URL(explicit, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin - : (typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000'); - // Use URL constructor so query strings in `path` (e.g. /api/v1/x?t=123) are handled correctly - return new URL(path, origin).toString(); + if (typeof window === 'undefined') { + // In non-browser contexts, avoid forcing any origin; return the relative path + return path; + } + const baseOrigin = explicit + ? new URL(explicit, window.location.origin).origin + : window.location.origin; + // Use URL constructor so query strings in `path` are handled correctly + return new URL(path, baseOrigin).toString(); } return path; } catch { diff --git a/frontend/src/setupProxy.js b/frontend/src/setupProxy.js index 5bf7d20..261dbf0 100644 --- a/frontend/src/setupProxy.js +++ b/frontend/src/setupProxy.js @@ -1,13 +1,13 @@ const { createProxyMiddleware } = require('http-proxy-middleware'); function resolveBackendOrigin() { - const raw = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1'; + const raw = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || 'http://127.0.0.1:8080/api/v1'; try { const u = new URL(raw); u.pathname = '/'; return u.toString(); } catch (e) { - return 'http://localhost:8080'; + return 'http://127.0.0.1:8080'; } } diff --git a/frontend/src/utils/url.ts b/frontend/src/utils/url.ts index 682caed..7d6f9dd 100644 --- a/frontend/src/utils/url.ts +++ b/frontend/src/utils/url.ts @@ -17,15 +17,7 @@ export function assetUrl(pathOrUrl?: string | null): string | undefined { baseUrl.pathname = '/'; return new URL(pathOrUrl, baseUrl).toString(); } - // 2) Local dev/IP: talk to backend:8080 directly - if (typeof window !== 'undefined') { - const { protocol, hostname } = window.location; - const isLocal = hostname === 'localhost' || /^\d{1,3}(\.\d{1,3}){3}$/.test(hostname); - if (isLocal) { - return `${protocol}//${hostname}:8080${pathOrUrl}`; - } - } - // 3) Production/domain: keep relative so frontend Nginx/Cloudflared path proxy forwards to backend + // 2) Keep relative so frontend dev proxy or edge proxy forwards to backend return pathOrUrl; } // Otherwise return as-is (relative or other paths) diff --git a/main.go b/main.go index 4a82489..5381e6b 100644 --- a/main.go +++ b/main.go @@ -120,12 +120,26 @@ func main() { // CORS: reflect the Origin only if it is allowed. In development, also allow localhost/127.0.0.1 any port. origin := c.Request.Header.Get("Origin") allowed := false + // 1) Explicit exact-origin allow list for _, ao := range config.AppConfig.AllowedOrigins { if ao == origin { allowed = true break } } + // 2) Wildcard support: ALLOWED_ORIGINS="*" means reflect any non-empty Origin + if !allowed { + for _, ao := range config.AppConfig.AllowedOrigins { + if ao == "*" && origin != "" { + allowed = true + break + } + } + } + // 3) If no ALLOWED_ORIGINS provided at all, reflect any non-empty Origin (useful for per-instance unknown domains) + if !allowed && len(config.AppConfig.AllowedOrigins) == 0 && origin != "" { + allowed = true + } if !allowed && origin != "" && config.AppConfig.AppEnv != "production" { // Relaxed rule for local dev if strings.HasPrefix(origin, "http://localhost:") || strings.HasPrefix(origin, "http://127.0.0.1:") || strings.HasPrefix(origin, "https://localhost:") || strings.HasPrefix(origin, "https://127.0.0.1:") {