mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
dev day #79
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user