// Package bootstrap // Path /internal/platform/bootstrap // File: loader.go // // Purpose: // Centralized application initializer (the “application kernel”). // This constructs and wires together the core runtime graph used by the // entire system: configuration, database, session manager (SCS), router (Gin), // CSRF wrapper (nosurf), and the HTTP server. // // Responsibilities: // 1) Load strongly-typed configuration. // 2) Initialize long-lived infrastructure (DB, sessions). // 3) Build the Gin router and mount global middleware and routes. // 4) Wrap the router with SCS (LoadAndSave) and CSRF in the correct order. // 5) Construct the http.Server and expose the assembled components via App. // // HTTP stack order (important): // Gin Router → SCS LoadAndSave → CSRF Wrapper → http.Server // // Design guarantees: // - Single source of truth via the App struct. // - Stable middleware order (SCS must wrap Gin before CSRF). // - Gin handlers can access *App via c.MustGet("app"). // - Extensible: add infra (cache/mailer/metrics) here. // // Change log: // [2025-10-24] Migrated to SCS-first wrapping and explicit App wiring. package bootstrap import ( "context" "database/sql" "encoding/gob" "fmt" "net/http" "time" weberr "synlotto-website/internal/http/error" databasePlatform "synlotto-website/internal/platform/database" "synlotto-website/internal/platform/config" "synlotto-website/internal/platform/csrf" "synlotto-website/internal/platform/session" "github.com/alexedwards/scs/v2" "github.com/gin-gonic/gin" _ "github.com/go-sql-driver/mysql" ) type App struct { Config config.Config DB *sql.DB SessionManager *scs.SessionManager Router *gin.Engine Handler http.Handler Server *http.Server } func Load(configPath string) (*App, error) { cfg, err := config.Load(configPath) if err != nil { return nil, fmt.Errorf("load config: %w", err) } db, err := openMySQL(cfg) if err != nil { return nil, err } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { return nil, fmt.Errorf("mysql ping: %w", err) } if err := databasePlatform.EnsureInitialSchema(db); err != nil { return nil, fmt.Errorf("ensure schema: %w", err) } gob.Register(map[string]string{}) gob.Register([]string{}) gob.Register(time.Time{}) sessions := session.New(cfg) router := gin.New() //router.Use(gin.Logger(), gin.Recovery()) router.Use(gin.Logger()) router.Static("/static", "./web/static") router.StaticFile("/favicon.ico", "./web/static/favicon.ico") app := &App{ Config: cfg, DB: db, SessionManager: sessions, Router: router, } router.Use(func(c *gin.Context) { c.Set("app", app) c.Next() }) router.NoRoute(weberr.NoRoute(app.SessionManager)) router.NoMethod(weberr.NoMethod(app.SessionManager)) router.Use(gin.CustomRecovery(weberr.Recovery(app.SessionManager))) handler := sessions.LoadAndSave(router) handler = csrf.Wrap(handler, cfg) addr := fmt.Sprintf("%s:%d", cfg.HttpServer.Address, cfg.HttpServer.Port) srv := &http.Server{ Addr: addr, Handler: handler, ReadHeaderTimeout: 10 * time.Second, // ToDo: consider moving to config } app.Handler = handler app.Server = srv return app, nil } func openMySQL(cfg config.Config) (*sql.DB, error) { dbCfg := cfg.Database escapedUser := dbCfg.Username escapedPass := dbCfg.Password dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&charset=utf8mb4,utf8&loc=UTC", escapedUser, escapedPass, dbCfg.Server, dbCfg.Port, dbCfg.DatabaseNamed, ) db, err := sql.Open("mysql", dsn) if err != nil { return nil, fmt.Errorf("mysql open: %w", err) } if dbCfg.MaxOpenConnections > 0 { db.SetMaxOpenConns(dbCfg.MaxOpenConnections) } if dbCfg.MaxIdleConnections > 0 { db.SetMaxIdleConns(dbCfg.MaxIdleConnections) } if dbCfg.ConnectionMaxLifetime != "" { if d, err := time.ParseDuration(dbCfg.ConnectionMaxLifetime); err == nil { db.SetConnMaxLifetime(d) } } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := db.PingContext(ctx); err != nil { _ = db.Close() return nil, fmt.Errorf("mysql ping: %w", err) } return db, nil }