Stack of changes to get gin, scs, nosurf running.
This commit is contained in:
@@ -1,30 +1,160 @@
|
||||
// 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 (
|
||||
"encoding/json"
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"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 AppState struct {
|
||||
Config *config.Config
|
||||
type App struct {
|
||||
Config config.Config
|
||||
DB *sql.DB
|
||||
SessionManager *scs.SessionManager
|
||||
Router *gin.Engine
|
||||
Handler http.Handler
|
||||
Server *http.Server
|
||||
}
|
||||
|
||||
func LoadAppState(configPath string) (*AppState, error) {
|
||||
file, err := os.Open(configPath)
|
||||
func Load(configPath string) (*App, error) {
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open config: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var config config.Config
|
||||
if err := json.NewDecoder(file).Decode(&config); err != nil {
|
||||
return nil, fmt.Errorf("decode config: %w", err)
|
||||
return nil, fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
|
||||
return &AppState{
|
||||
Config: &config,
|
||||
}, nil
|
||||
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)
|
||||
}
|
||||
|
||||
sessions := session.New(cfg)
|
||||
|
||||
router := gin.New()
|
||||
router.Use(gin.Logger(), gin.Recovery())
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user