mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
@@ -54,12 +54,12 @@ NEWSLETTER_ENABLED=true
|
|||||||
|
|
||||||
# File Uploads
|
# File Uploads
|
||||||
UPLOAD_DIR=./uploads
|
UPLOAD_DIR=./uploads
|
||||||
MAX_UPLOAD_SIZE=10485760 # 10MB
|
MAX_UPLOAD_SIZE=50
|
||||||
ALLOWED_FILE_TYPES=image/jpeg,image/png,image/gif,application/pdf
|
ALLOWED_FILE_TYPES=image/jpeg,image/png,image/gif,application/pdf
|
||||||
MAX_FILES=5 # Maximum number of files per upload
|
MAX_FILES=5 # Maximum number of files per upload
|
||||||
|
|
||||||
# CORS
|
# CORS
|
||||||
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080
|
ALLOWED_ORIGINS=*
|
||||||
# Frontend Configuration
|
# Frontend Configuration
|
||||||
REACT_APP_NAME=Fotbal Club Manager
|
REACT_APP_NAME=Fotbal Club Manager
|
||||||
REACT_APP_API_URL=/api/v1
|
REACT_APP_API_URL=/api/v1
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
VStack,
|
VStack,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { Download, ExternalLink } from 'lucide-react';
|
import { Download, ExternalLink } from 'lucide-react';
|
||||||
|
import { API_URL } from '../../services/api';
|
||||||
|
|
||||||
interface PhotoModalProps {
|
interface PhotoModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -34,8 +35,7 @@ const PhotoModal: React.FC<PhotoModalProps> = ({
|
|||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
const getProxyUrl = (url: string) => {
|
const getProxyUrl = (url: string) => {
|
||||||
const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080';
|
return `${API_URL}/gallery/proxy-image?url=${encodeURIComponent(url)}`;
|
||||||
return `${apiUrl}/api/v1/gallery/proxy-image?url=${encodeURIComponent(url)}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export const MatchesWidget = () => {
|
|||||||
const resolveUrl = (path: string) => {
|
const resolveUrl = (path: string) => {
|
||||||
try {
|
try {
|
||||||
if (/^https?:\/\//i.test(path)) return path;
|
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;
|
return origin + path;
|
||||||
} catch {
|
} catch {
|
||||||
return path;
|
return path;
|
||||||
@@ -108,7 +108,7 @@ export const MatchesWidget = () => {
|
|||||||
}
|
}
|
||||||
const chosen = candidate || orig;
|
const chosen = candidate || orig;
|
||||||
if (typeof chosen === 'string' && chosen.startsWith('/')) {
|
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 origin + chosen;
|
||||||
}
|
}
|
||||||
return chosen || (assetUrl('/dist/img/logo-club-empty.svg') as string);
|
return chosen || (assetUrl('/dist/img/logo-club-empty.svg') as string);
|
||||||
@@ -120,7 +120,7 @@ export const MatchesWidget = () => {
|
|||||||
queryKey: ['upcomingMatchesCache'],
|
queryKey: ['upcomingMatchesCache'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
// Build absolute origin from API URL env (which may include /api/v1)
|
// 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 url = `${origin}/cache/prefetch/facr_club_info.json`;
|
||||||
|
|
||||||
const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } });
|
const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } });
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// API configuration
|
// 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
|
// App configuration
|
||||||
export const APP_NAME = 'Fotbal Club';
|
export const APP_NAME = 'Fotbal Club';
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const ArticleDetailPage: React.FC = () => {
|
|||||||
queryKey: ['facr-cached-match', (matchLinkQuery.data as any)?.external_match_id],
|
queryKey: ['facr-cached-match', (matchLinkQuery.data as any)?.external_match_id],
|
||||||
enabled: Boolean((matchLinkQuery.data as any)?.external_match_id),
|
enabled: Boolean((matchLinkQuery.data as any)?.external_match_id),
|
||||||
queryFn: async () => {
|
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 url = `${origin}/cache/prefetch/facr_club_info.json`;
|
||||||
const res = await fetch(url, { cache: 'no-cache' });
|
const res = await fetch(url, { cache: 'no-cache' });
|
||||||
if (!res.ok) return null as any;
|
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(API_URL, window.location.origin).origin;
|
||||||
const origin = new URL(apiUrl).origin;
|
|
||||||
|
|
||||||
// Try to find album in both sources
|
// Try to find album in both sources
|
||||||
const [profileRes, albumsRes] = await Promise.allSettled([
|
const [profileRes, albumsRes] = await Promise.allSettled([
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { assetUrl, sanitizeClubName } from '../utils/url';
|
|||||||
import MatchModal from '../components/home/MatchModal';
|
import MatchModal from '../components/home/MatchModal';
|
||||||
import { useCountdown, useMultipleCountdowns } from '../hooks/useCountdown';
|
import { useCountdown, useMultipleCountdowns } from '../hooks/useCountdown';
|
||||||
import '../styles/theme.css';
|
import '../styles/theme.css';
|
||||||
|
import { API_URL } from '../services/api';
|
||||||
|
|
||||||
type MatchItem = {
|
type MatchItem = {
|
||||||
id: number | string;
|
id: number | string;
|
||||||
@@ -225,8 +226,7 @@ const MatchesPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
if (/^https?:\/\//i.test(path)) return path;
|
if (/^https?:\/\//i.test(path)) return path;
|
||||||
if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) {
|
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(API_URL, typeof window !== 'undefined' ? window.location.origin : undefined);
|
||||||
const b = new URL(base);
|
|
||||||
const abs = new URL(path, `${b.protocol}//${b.host}`);
|
const abs = new URL(path, `${b.protocol}//${b.host}`);
|
||||||
return abs.toString();
|
return abs.toString();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ClubModal from '../components/home/ClubModal';
|
|||||||
import { usePublicSettings } from '../hooks/usePublicSettings';
|
import { usePublicSettings } from '../hooks/usePublicSettings';
|
||||||
import { sortCategoriesWithOrder } from '../utils/categorySort';
|
import { sortCategoriesWithOrder } from '../utils/categorySort';
|
||||||
import { TeamLogo } from '../components/common/TeamLogo';
|
import { TeamLogo } from '../components/common/TeamLogo';
|
||||||
|
import { API_URL } from '../services/api';
|
||||||
|
|
||||||
type TableRow = {
|
type TableRow = {
|
||||||
rank: string;
|
rank: string;
|
||||||
@@ -34,10 +35,9 @@ const resolveBackendUrl = (path: string) => {
|
|||||||
try {
|
try {
|
||||||
if (/^https?:\/\//i.test(path)) return path;
|
if (/^https?:\/\//i.test(path)) return path;
|
||||||
if (path.startsWith('/cache') || path.startsWith('/uploads')) {
|
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(API_URL, typeof window !== 'undefined' ? window.location.origin : undefined);
|
||||||
const u = new URL(base);
|
const abs = new URL(path, `${u.protocol}//${u.host}`);
|
||||||
u.pathname = path;
|
return abs.toString();
|
||||||
return u.toString();
|
|
||||||
}
|
}
|
||||||
return path;
|
return path;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const SponsorsAdminPage: React.FC = () => {
|
|||||||
const normalizeImageUrl = (url?: string) => {
|
const normalizeImageUrl = (url?: string) => {
|
||||||
if (!url || url === '') return '/logo192.png';
|
if (!url || url === '') return '/logo192.png';
|
||||||
if (/^https?:\/\//i.test(url)) return url;
|
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}`;
|
if (url.startsWith('/uploads/')) return `${origin}${url}`;
|
||||||
return `${origin}${url.startsWith('/') ? '' : '/'}${url}`;
|
return `${origin}${url.startsWith('/') ? '' : '/'}${url}`;
|
||||||
};
|
};
|
||||||
@@ -129,8 +129,7 @@ const SponsorsAdminPage: React.FC = () => {
|
|||||||
if (!file) return;
|
if (!file) return;
|
||||||
try {
|
try {
|
||||||
const res = await uploadFile(file);
|
const res = await uploadFile(file);
|
||||||
const apiUrl = process.env.REACT_APP_API_URL || 'http://localhost:8080/api/v1';
|
const apiOrigin = new URL(API_URL, window.location.origin).origin;
|
||||||
const apiOrigin = new URL(apiUrl).origin;
|
|
||||||
// If backend returned an absolute URL pointing to the dev host (same origin as app), rewrite to API origin
|
// If backend returned an absolute URL pointing to the dev host (same origin as app), rewrite to API origin
|
||||||
let url = res.url || '';
|
let url = res.url || '';
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const StandingsAdminPage: React.FC = () => {
|
|||||||
const { data, isLoading, error } = useQuery<any>({
|
const { data, isLoading, error } = useQuery<any>({
|
||||||
queryKey: ['facr-tables-cache'],
|
queryKey: ['facr-tables-cache'],
|
||||||
queryFn: async () => {
|
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 url = `${origin}/cache/prefetch/facr_tables.json`;
|
||||||
const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } });
|
const res = await fetch(url, { headers: { 'Cache-Control': 'no-cache' } });
|
||||||
if (!res.ok) throw new Error(`Failed to load cache: ${res.status}`);
|
if (!res.ok) throw new Error(`Failed to load cache: ${res.status}`);
|
||||||
|
|||||||
@@ -31,11 +31,15 @@ const resolveBackendUrl = (path: string) => {
|
|||||||
if (/^https?:\/\//i.test(path)) return path;
|
if (/^https?:\/\//i.test(path)) return path;
|
||||||
if (path.startsWith('/cache') || path.startsWith('/uploads') || path.startsWith('/api/')) {
|
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 explicit = process.env.REACT_APP_API_BASE_URL || process.env.REACT_APP_API_URL || '';
|
||||||
const origin = explicit
|
if (typeof window === 'undefined') {
|
||||||
? new URL(explicit, typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000').origin
|
// In non-browser contexts, avoid forcing any origin; return the relative path
|
||||||
: (typeof window !== 'undefined' ? window.location.origin : 'http://localhost:3000');
|
return path;
|
||||||
// Use URL constructor so query strings in `path` (e.g. /api/v1/x?t=123) are handled correctly
|
}
|
||||||
return new URL(path, origin).toString();
|
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;
|
return path;
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
const { createProxyMiddleware } = require('http-proxy-middleware');
|
const { createProxyMiddleware } = require('http-proxy-middleware');
|
||||||
|
|
||||||
function resolveBackendOrigin() {
|
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 {
|
try {
|
||||||
const u = new URL(raw);
|
const u = new URL(raw);
|
||||||
u.pathname = '/';
|
u.pathname = '/';
|
||||||
return u.toString();
|
return u.toString();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'http://localhost:8080';
|
return 'http://127.0.0.1:8080';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,15 +17,7 @@ export function assetUrl(pathOrUrl?: string | null): string | undefined {
|
|||||||
baseUrl.pathname = '/';
|
baseUrl.pathname = '/';
|
||||||
return new URL(pathOrUrl, baseUrl).toString();
|
return new URL(pathOrUrl, baseUrl).toString();
|
||||||
}
|
}
|
||||||
// 2) Local dev/IP: talk to backend:8080 directly
|
// 2) Keep relative so frontend dev proxy or edge proxy forwards to backend
|
||||||
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
|
|
||||||
return pathOrUrl;
|
return pathOrUrl;
|
||||||
}
|
}
|
||||||
// Otherwise return as-is (relative or other paths)
|
// Otherwise return as-is (relative or other paths)
|
||||||
|
|||||||
@@ -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.
|
// 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")
|
origin := c.Request.Header.Get("Origin")
|
||||||
allowed := false
|
allowed := false
|
||||||
|
// 1) Explicit exact-origin allow list
|
||||||
for _, ao := range config.AppConfig.AllowedOrigins {
|
for _, ao := range config.AppConfig.AllowedOrigins {
|
||||||
if ao == origin {
|
if ao == origin {
|
||||||
allowed = true
|
allowed = true
|
||||||
break
|
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" {
|
if !allowed && origin != "" && config.AppConfig.AppEnv != "production" {
|
||||||
// Relaxed rule for local dev
|
// 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:") {
|
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:") {
|
||||||
|
|||||||
Reference in New Issue
Block a user