167 lines
4.3 KiB
Go
167 lines
4.3 KiB
Go
// 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
|
|
}
|