This commit is contained in:
Tomas Dvorak
2025-11-02 01:04:02 +01:00
parent ac886502e0
commit b9cea0cd77
153 changed files with 43713 additions and 1700 deletions
+85 -29
View File
@@ -11,9 +11,11 @@ import (
"path/filepath"
"syscall"
"time"
"net/url"
"fotbal-club/internal/config"
"fotbal-club/internal/controllers"
"fotbal-club/internal/middleware"
"fotbal-club/internal/models"
"fotbal-club/internal/routes"
"fotbal-club/internal/services"
@@ -38,6 +40,11 @@ func main() {
logger.SetLevel(logger.LevelInfo)
}
// Gin mode: use release in production for performance
if config.AppConfig != nil && config.AppConfig.AppEnv == "production" {
gin.SetMode(gin.ReleaseMode)
}
// Normalize and ensure upload directory exists early
uploadDir := config.AppConfig.UploadDir
if strings.TrimSpace(uploadDir) == "" {
@@ -85,6 +92,19 @@ func main() {
&models.PageElementConfig{},
&models.ShortLink{},
&models.LinkClick{},
&models.Comment{},
&models.CommentReaction{},
&models.CommentBan{},
&models.UnbanRequest{},
&models.CommentReport{},
&models.UserProfile{},
&models.PointsTransaction{},
&models.Achievement{},
&models.UserAchievement{},
&models.RewardItem{},
&models.RewardRedemption{},
&models.UploadedFile{},
&models.FileUsage{},
); err != nil {
log.Printf("Warning: AutoMigrate failed: %v", err)
}
@@ -97,6 +117,24 @@ func main() {
} else {
log.Printf("[startup] NewsletterEnabled=%v (from env default)", config.AppConfig.NewsletterEnabled)
}
// Auto-append FrontendBaseURL origin from settings to CORS AllowedOrigins
if strings.TrimSpace(settings.FrontendBaseURL) != "" {
if u, err := url.Parse(settings.FrontendBaseURL); err == nil && u.Scheme != "" && u.Host != "" {
origin := u.Scheme + "://" + u.Host
found := false
for _, ao := range config.AppConfig.AllowedOrigins {
if ao == origin {
found = true
break
}
}
if !found {
config.AppConfig.AllowedOrigins = append(config.AppConfig.AllowedOrigins, origin)
log.Printf("[startup] Appended CORS allowed origin from settings: %s", origin)
}
}
}
}
// Check if we should seed the database
@@ -108,8 +146,24 @@ func main() {
}
}
// Initialize Gin router
r := gin.Default()
// Initialize Gin router (custom stack)
r := gin.New()
// Use custom recovery with request ID tracking (replaces gin.Recovery)
r.Use(middleware.CustomRecovery())
// Do not trust any proxies by default (prevent spoofed client IP)
if err := r.SetTrustedProxies(nil); err != nil {
log.Printf("Trusted proxies setup error: %v", err)
}
// Lightweight hardening middlewares
r.Use(middleware.RequestID())
r.Use(middleware.RequestLogger())
r.Use(middleware.SanitizeHeaders())
// Add database context with timeout to prevent hanging queries
r.Use(middleware.DBContext())
// Limit non-upload request bodies (2MB)
r.Use(middleware.RequestSizeLimit(2 * 1024 * 1024))
// Enforce JSON for mutating API calls (uploads exempt)
r.Use(middleware.ValidateContentType())
// Set max multipart memory to match upload size limit (default is 32MB)
r.MaxMultipartMemory = config.AppConfig.MaxUploadSize
@@ -117,33 +171,23 @@ func main() {
// Enable gzip compression for responses
r.Use(gzip.Gzip(gzip.DefaultCompression))
// Security headers & CORS
r.Use(func(c *gin.Context) {
// Security headers
c.Writer.Header().Set("X-Content-Type-Options", "nosniff")
c.Writer.Header().Set("X-Frame-Options", "SAMEORIGIN")
c.Writer.Header().Set("Referrer-Policy", "no-referrer-when-downgrade")
// Add HSTS when using HTTPS (including behind a proxy)
if c.Request.TLS != nil || c.Request.Header.Get("X-Forwarded-Proto") == "https" {
c.Writer.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains; preload")
}
// Content Security Policy from configuration (override via CONTENT_SECURITY_POLICY)
if config.AppConfig.ContentSecurityPolicy != "" {
c.Writer.Header().Set("Content-Security-Policy", config.AppConfig.ContentSecurityPolicy)
}
// Apply strict security headers
r.Use(middleware.SecurityHeaders())
// 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 {
// CORS only (security headers handled by middleware.SecurityHeaders)
r.Use(func(c *gin.Context) {
// 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 {
if !allowed {
for _, ao := range config.AppConfig.AllowedOrigins {
if ao == "*" && origin != "" {
allowed = true
@@ -151,13 +195,16 @@ func main() {
}
}
}
// 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 != "" {
// 3) If no ALLOWED_ORIGINS provided at all, reflect any non-empty Origin only in non-production
if !allowed && len(config.AppConfig.AllowedOrigins) == 0 && origin != "" && config.AppConfig.AppEnv != "production" {
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:") {
// Relaxed rule for local dev, including common private LAN ranges
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:") ||
strings.HasPrefix(origin, "http://192.168.") || strings.HasPrefix(origin, "https://192.168.") ||
strings.HasPrefix(origin, "http://10.") || strings.HasPrefix(origin, "https://10.") ||
strings.HasPrefix(origin, "http://172.") || strings.HasPrefix(origin, "https://172.") {
allowed = true
}
}
@@ -165,9 +212,11 @@ func main() {
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
c.Writer.Header().Set("Vary", "Origin")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
// Expose request id header for client-side diagnostics
c.Writer.Header().Set("Access-Control-Expose-Headers", "X-Request-ID")
}
c.Writer.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, Cache-Control, X-Requested-With, X-Session-Token, X-Admin-Token, X-Dev-Admin")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, Cache-Control, X-Requested-With, X-Session-Token, X-Admin-Token, X-Dev-Admin, X-CSRF-Token")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
@@ -175,7 +224,10 @@ func main() {
}
c.Next()
})
})
// Set optimal caching for static assets and generated caches
r.Use(middleware.AssetCacheControl())
// Setup API routes
api := r.Group("/api/v1")
@@ -229,6 +281,10 @@ func main() {
srv := &http.Server{
Addr: ":" + port,
Handler: r,
ReadTimeout: config.AppConfig.ReadTimeout,
ReadHeaderTimeout: 10 * time.Second,
WriteTimeout: config.AppConfig.WriteTimeout,
IdleTimeout: config.AppConfig.IdleTimeout,
}
// DB handle for closing on shutdown