mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
upload
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"fotbal-club/internal/config"
|
||||
"fotbal-club/internal/controllers"
|
||||
"fotbal-club/internal/models"
|
||||
"fotbal-club/internal/routes"
|
||||
"fotbal-club/internal/services"
|
||||
db "fotbal-club/pkg/database"
|
||||
"fotbal-club/pkg/email"
|
||||
"fotbal-club/pkg/logger"
|
||||
|
||||
"github.com/gin-contrib/gzip"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load configuration
|
||||
config.LoadConfig()
|
||||
|
||||
// Set logger level based on DEBUG flag
|
||||
if config.AppConfig != nil && config.AppConfig.Debug {
|
||||
logger.SetLevel(logger.LevelDebug)
|
||||
log.Println("Logger level set to DEBUG")
|
||||
} else {
|
||||
logger.SetLevel(logger.LevelInfo)
|
||||
}
|
||||
|
||||
// Initialize database
|
||||
dbInstance, err := db.InitDB()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
// Check if we should run migrations
|
||||
runMigrations, _ := strconv.ParseBool(os.Getenv("RUN_MIGRATIONS"))
|
||||
if runMigrations {
|
||||
if err := db.MigrateDB(dbInstance); err != nil {
|
||||
log.Fatalf("Failed to run database migrations: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Best-effort: ensure schemas exist and are up-to-date (adds new columns)
|
||||
if err := dbInstance.AutoMigrate(
|
||||
&models.Settings{},
|
||||
&models.Article{},
|
||||
&models.ScoreboardState{},
|
||||
&models.CompetitionAlias{},
|
||||
&models.Player{},
|
||||
&models.ContactCategory{},
|
||||
&models.Contact{},
|
||||
&models.ContactMessage{},
|
||||
&models.NewsletterSubscription{},
|
||||
&models.Sponsor{},
|
||||
&models.Clothing{},
|
||||
&models.Poll{},
|
||||
&models.PollOption{},
|
||||
&models.PollVote{},
|
||||
&models.NavigationItem{},
|
||||
&models.SocialLink{},
|
||||
&models.PageElementConfig{},
|
||||
); err != nil {
|
||||
log.Printf("Warning: AutoMigrate failed: %v", err)
|
||||
}
|
||||
var settings models.Settings
|
||||
_ = dbInstance.First(&settings).Error // ignore not found
|
||||
if config.AppConfig != nil {
|
||||
if settings.ID != 0 {
|
||||
config.AppConfig.NewsletterEnabled = settings.NewsletterEnabled
|
||||
log.Printf("[startup] NewsletterEnabled=%v (from settings)", settings.NewsletterEnabled)
|
||||
} else {
|
||||
log.Printf("[startup] NewsletterEnabled=%v (from env default)", config.AppConfig.NewsletterEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should seed the database
|
||||
seedDatabase, _ := strconv.ParseBool(os.Getenv("SEED_DATABASE"))
|
||||
if seedDatabase {
|
||||
log.Println("Seeding database...")
|
||||
if err := db.SeedDB(dbInstance); err != nil {
|
||||
log.Printf("Warning: Failed to seed database: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize Gin router
|
||||
r := gin.Default()
|
||||
|
||||
// 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", "DENY")
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
for _, ao := range config.AppConfig.AllowedOrigins {
|
||||
if ao == origin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
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:") {
|
||||
allowed = true
|
||||
}
|
||||
}
|
||||
if allowed && origin != "" {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
c.Writer.Header().Set("Vary", "Origin")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
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")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
})
|
||||
|
||||
// Setup API routes
|
||||
api := r.Group("/api/v1")
|
||||
|
||||
// Setup other routes
|
||||
routes.SetupRoutes(api, dbInstance)
|
||||
routes.SetupRootRoutes(r, dbInstance)
|
||||
|
||||
// Prometheus metrics endpoint
|
||||
r.GET("/metrics", gin.WrapH(promhttp.Handler()))
|
||||
|
||||
// Create an email service instance for background jobs (uses DB-backed SMTP settings)
|
||||
emailSvc := email.NewEmailService(config.AppConfig, dbInstance)
|
||||
|
||||
// Expose cached JSON files for the frontend at /cache/*
|
||||
r.Static("/cache", "./cache")
|
||||
|
||||
// Serve static assets and uploads
|
||||
// Map /dist to ./static to expose files like /dist/img/logo-club-empty.svg
|
||||
r.Static("/dist", "./static")
|
||||
r.Static("/uploads", "./uploads")
|
||||
|
||||
// Ensure gallery flat files exist at startup (best effort)
|
||||
_ = services.RegenerateFlatGalleryFiles()
|
||||
|
||||
// Start background prefetcher (fetches public endpoints to cache JSON every 30 minutes)
|
||||
// It targets this server's own public API. Allow override via PREFETCH_TARGET (useful in container/proxy setups).
|
||||
prefetchTarget := os.Getenv("PREFETCH_TARGET")
|
||||
if prefetchTarget == "" {
|
||||
prefetchTarget = "http://127.0.0.1:" + config.AppConfig.Port + "/api/v1"
|
||||
}
|
||||
services.StartPrefetcher(prefetchTarget)
|
||||
|
||||
// Start newsletter scheduler (automated emails - legacy weekly)
|
||||
services.StartNewsletterScheduler(dbInstance, emailSvc)
|
||||
|
||||
// Start comprehensive newsletter automation (weekly, match alerts, blog notifications, results)
|
||||
newsletterAutomation := services.NewNewsletterAutomation(dbInstance, emailSvc)
|
||||
newsletterAutomation.Start()
|
||||
|
||||
// Store newsletter automation instance for use in controllers
|
||||
controllers.SetNewsletterAutomation(newsletterAutomation)
|
||||
|
||||
// Start server with graceful shutdown
|
||||
port := config.AppConfig.Port
|
||||
// Fail fast in production if JWT secret is left as default
|
||||
if config.AppConfig.AppEnv == "production" && config.AppConfig.JWTSecret == "default-secret-key-change-in-production" {
|
||||
log.Fatalf("SECURITY: JWT_SECRET is using the default value in production. Please set a strong secret.")
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
// DB handle for closing on shutdown
|
||||
sqlDB, err := dbInstance.DB()
|
||||
if err != nil {
|
||||
log.Printf("Error getting database connection: %v", err)
|
||||
}
|
||||
|
||||
// Start server in background
|
||||
go func() {
|
||||
log.Printf("Server starting on port %s\n", port)
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for interrupt signal to gracefully shutdown the server
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Println("Shutdown signal received, shutting down server...")
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Printf("Server forced to shutdown: %v", err)
|
||||
}
|
||||
|
||||
if sqlDB != nil {
|
||||
if err := sqlDB.Close(); err != nil {
|
||||
log.Printf("Error closing database connection: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user