package main import ( "context" "errors" "log" "net/http" "os" "os/signal" "syscall" "time" "github.com/juanatsap/cv-site/internal/config" "github.com/juanatsap/cv-site/internal/handlers" "github.com/juanatsap/cv-site/internal/middleware" "github.com/juanatsap/cv-site/internal/models" "github.com/juanatsap/cv-site/internal/templates" ) const version = "1.0.0" func main() { // Initialize logger log.SetFlags(log.LstdFlags | log.Lshortfile) log.Println("🚀 Starting CV Server v" + version) // Load configuration cfg := config.Load() log.Printf("✓ Configuration loaded (env: %s)", os.Getenv("GO_ENV")) // Initialize cache (1 hour TTL, configurable via env) cacheTTL := 1 * time.Hour if ttlEnv := os.Getenv("CACHE_TTL_MINUTES"); ttlEnv != "" { if minutes, err := time.ParseDuration(ttlEnv + "m"); err == nil { cacheTTL = minutes } } models.InitCache(cacheTTL) // Warm cache with default languages log.Println("🔥 Warming cache...") for _, lang := range []string{"en", "es"} { // Warm CV cache if _, err := models.LoadCV(lang); err != nil { log.Printf("⚠️ Failed to warm CV cache for %s: %v", lang, err) } // Warm UI cache if _, err := models.LoadUI(lang); err != nil { log.Printf("⚠️ Failed to warm UI cache for %s: %v", lang, err) } } log.Printf("✓ Cache warmed (TTL: %v)", cacheTTL) // Initialize template manager templateMgr, err := templates.NewManager(&cfg.Template) if err != nil { log.Fatalf("❌ Failed to initialize templates: %v", err) } // Initialize handlers cvHandler := handlers.NewCVHandler(templateMgr, cfg.Address()) healthHandler := handlers.NewHealthHandler(version) // Setup router mux := http.NewServeMux() // Create rate limiter for PDF endpoint // Allow 3 PDF generations per minute per IP pdfRateLimiter := middleware.NewRateLimiter(3, 1*time.Minute) // Routes mux.HandleFunc("/", cvHandler.Home) mux.HandleFunc("/cv", cvHandler.CVContent) mux.HandleFunc("/health", healthHandler.Check) // Protected PDF endpoint with origin checking + rate limiting protectedPDFHandler := middleware.OriginChecker( pdfRateLimiter.Middleware( http.HandlerFunc(cvHandler.ExportPDF), ), ) mux.Handle("/export/pdf", protectedPDFHandler) // Static files with cache control staticHandler := http.StripPrefix("/static/", http.FileServer(http.Dir("static"))) mux.Handle("/static/", cacheControl(staticHandler)) // Apply middleware chain handler := middleware.Recovery( middleware.Logger( middleware.SecurityHeaders(mux), ), ) // Create server with timeouts server := &http.Server{ Addr: ":" + cfg.Server.Port, Handler: handler, ReadTimeout: time.Duration(cfg.Server.ReadTimeout) * time.Second, WriteTimeout: time.Duration(cfg.Server.WriteTimeout) * time.Second, IdleTimeout: 120 * time.Second, } // Start server in goroutine serverErrors := make(chan error, 1) go func() { log.Printf("✓ Server listening on http://%s:%s", cfg.Server.Host, cfg.Server.Port) log.Printf("📄 English: http://%s:%s/?lang=en", cfg.Server.Host, cfg.Server.Port) log.Printf("📄 Spanish: http://%s:%s/?lang=es", cfg.Server.Host, cfg.Server.Port) log.Printf("❤️ Health: http://%s:%s/health", cfg.Server.Host, cfg.Server.Port) log.Println("Press Ctrl+C to shutdown") serverErrors <- server.ListenAndServe() }() // Setup graceful shutdown shutdown := make(chan os.Signal, 1) signal.Notify(shutdown, os.Interrupt, syscall.SIGTERM) // Wait for shutdown signal or server error select { case err := <-serverErrors: if !errors.Is(err, http.ErrServerClosed) { log.Fatalf("❌ Server error: %v", err) } case sig := <-shutdown: log.Printf("🛑 Shutdown signal received: %v", sig) // Create shutdown context with timeout ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // Attempt graceful shutdown if err := server.Shutdown(ctx); err != nil { log.Printf("⚠️ Graceful shutdown failed, forcing: %v", err) if err := server.Close(); err != nil { log.Fatalf("❌ Failed to close server: %v", err) } } log.Println("✓ Server stopped gracefully") } } // cacheControl adds cache headers to static files func cacheControl(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Cache static files for 1 hour in development, 1 day in production maxAge := "3600" // 1 hour if os.Getenv("GO_ENV") == "production" { maxAge = "86400" // 1 day } w.Header().Set("Cache-Control", "public, max-age="+maxAge) h.ServeHTTP(w, r) }) }