Code documentation
This commit is contained in:
@@ -1,3 +1,37 @@
|
||||
// Package config
|
||||
// Path: /internal/platform/config
|
||||
// File: config.go
|
||||
//
|
||||
// Purpose
|
||||
// Provide a safe one-time initialization and global access point for
|
||||
// the application's Config object, once it has been constructed during
|
||||
// bootstrap.
|
||||
//
|
||||
// This allows other packages to retrieve configuration without needing
|
||||
// dependency injection at every call site, while still preventing
|
||||
// accidental mutation after init.
|
||||
//
|
||||
// Responsibilities (as implemented here)
|
||||
// 1) Store a single *Config instance for the lifetime of the process.
|
||||
// 2) Ensure Init() can only succeed once via sync.Once.
|
||||
// 3) Expose Get() as a global accessor.
|
||||
//
|
||||
// Design notes
|
||||
// - Config is written once at startup via Init() inside bootstrap.
|
||||
// - Calls to Init() after the first are ignored silently.
|
||||
// - Get() may return nil if called before Init() — caller must ensure
|
||||
// bootstrap has completed.
|
||||
//
|
||||
// TODOs (from current architectural direction)
|
||||
// - Evaluate replacing global access with explicit dependency injection
|
||||
// in future modules for stronger compile-time guarantees.
|
||||
// - Consider panicking or logging if Get() is called before Init().
|
||||
// - Move non-static configuration into runtime struct(s) owned by App.
|
||||
// - Ensure immutability: avoid mutating Config fields after Init().
|
||||
//
|
||||
// Change log
|
||||
// [2025-10-28] Documentation aligned with real runtime responsibilities.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
// Package config
|
||||
// Path: /internal/platform/config
|
||||
// File: load.go
|
||||
//
|
||||
// Purpose
|
||||
// Responsible solely for loading strongly-typed application configuration
|
||||
// from a JSON file on disk. This is the *input* stage of configuration
|
||||
// lifecycle — the resulting Config is consumed by bootstrap and may be
|
||||
// optionally stored globally via config.Init().
|
||||
//
|
||||
// Responsibilities (as implemented here)
|
||||
// 1) Read configuration JSON file from a specified path.
|
||||
// 2) Deserialize into the Config struct (strongly typed).
|
||||
// 3) Return the populated Config value or an error.
|
||||
//
|
||||
// Design notes
|
||||
// - Path is caller-controlled (bootstrap decides where config.json lives).
|
||||
// - No defaults or validation are enforced here — errors bubble to bootstrap.
|
||||
// - Pure function: no globals mutated, safe for tests and reuse.
|
||||
// - Load returns a **value**, not a pointer, avoiding accidental mutation
|
||||
// unless caller explicitly stores it.
|
||||
//
|
||||
// TODOs (from current architecture direction)
|
||||
// - Add schema validation for required config fields.
|
||||
// - Add environment override support for deployment flexibility.
|
||||
// - Consider merging with a future layered config system (file + env + flags).
|
||||
// - Emit structured errors including path details for troubleshooting.
|
||||
//
|
||||
// Change log
|
||||
// [2025-10-29] Documentation aligned with bootstrap integration and config.Init() use.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
@@ -5,6 +36,11 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// Load reads the JSON configuration file located at `path`
|
||||
// and unmarshals it into a Config struct.
|
||||
//
|
||||
// Caller is responsible for passing the result into bootstrap and/or
|
||||
// config.Init() to make it globally available.
|
||||
func Load(path string) (Config, error) {
|
||||
var cfg Config
|
||||
|
||||
|
||||
@@ -1,40 +1,78 @@
|
||||
// Package config
|
||||
// Path: /internal/platform/config
|
||||
// File: types.go
|
||||
//
|
||||
// Purpose
|
||||
// Defines the strongly-typed configuration structure for the entire system.
|
||||
// Populated from JSON via config.Load() and stored in bootstrap for use by:
|
||||
// - MySQL connectivity + pooling
|
||||
// - HTTP server binding + security mode
|
||||
// - SCS session configuration
|
||||
// - CSRF cookie policy
|
||||
// - External licensing API configuration
|
||||
// - Template meta (site-wide branding)
|
||||
//
|
||||
// Design notes
|
||||
// - Nested struct fields map directly to JSON blocks.
|
||||
// - Types are primarily string-based for durations (parsed by bootstrap).
|
||||
// - Field names reflect actual usage in the code today.
|
||||
// - All configuration values are held immutable after bootstrap.Load.
|
||||
//
|
||||
// TODOs (observations from current design)
|
||||
// - Add `json:"database"` tag to Database struct for JSON consistency
|
||||
// (currently missing; loader still works due to exported field name fallback).
|
||||
// - Validate required fields at bootstrap (server/port/site name).
|
||||
// - Move sensitive fields (password/API key) to env-driven overrides.
|
||||
// - Convert duration strings to `time.Duration` when loading (type-safe).
|
||||
//
|
||||
// Change log
|
||||
// [2025-10-29] Documentation created to align with full settings usage.
|
||||
|
||||
package config
|
||||
|
||||
// Config represents all runtime configuration for the application.
|
||||
// Loaded from JSON and passed into bootstrap for wiring platform components.
|
||||
type Config struct {
|
||||
// CSRF cookie naming and storage controls
|
||||
CSRF struct {
|
||||
CookieName string `json:"cookieName"`
|
||||
} `json:"csrf"`
|
||||
|
||||
// Database connection settings + tuning
|
||||
Database struct {
|
||||
Server string `json:"server"`
|
||||
Port int `json:"port"`
|
||||
DatabaseNamed string `json:"databaseName"`
|
||||
MaxOpenConnections int `json:"maxOpenConnections"`
|
||||
MaxIdleConnections int `json:"maxIdleConnections"`
|
||||
ConnectionMaxLifetime string `json:"connectionMaxLifetime"`
|
||||
DatabaseName string `json:"databaseName"`
|
||||
MaxOpenConnections int `json:"maxOpenConnections"` // optional tuning
|
||||
MaxIdleConnections int `json:"maxIdleConnections"` // optional tuning
|
||||
ConnectionMaxLifetime string `json:"connectionMaxLifetime"` // duration as string, parsed in bootstrap
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Password string `json:"password"` // sensitive; consider environment secrets
|
||||
}
|
||||
|
||||
// HTTP server exposure and security toggles
|
||||
HttpServer struct {
|
||||
Port int `json:"port"`
|
||||
Address string `json:"address"`
|
||||
ProductionMode bool `json:"productionMode"`
|
||||
ProductionMode bool `json:"productionMode"` // controls Secure cookie flag
|
||||
} `json:"httpServer"`
|
||||
|
||||
// Remote licensing API service configuration
|
||||
License struct {
|
||||
APIURL string `json:"apiUrl"`
|
||||
APIKey string `json:"apiKey"`
|
||||
APIKey string `json:"apiKey"` // sensitive; consider environment secrets
|
||||
} `json:"license"`
|
||||
|
||||
// Session (SCS) configuration: cookie names, lifetime + idle timeout
|
||||
Session struct {
|
||||
CookieName string `json:"cookieName"`
|
||||
Lifetime string `json:"lifetime"`
|
||||
IdleTimeout string `json:"idleTimeout"`
|
||||
Lifetime string `json:"lifetime"` // duration as string; parsed in platform/session
|
||||
IdleTimeout string `json:"idleTimeout"` // duration as string; parsed in platform/session
|
||||
RememberCookieName string `json:"rememberCookieName"`
|
||||
RememberDuration string `json:"rememberDuration"`
|
||||
RememberDuration string `json:"rememberDuration"` // duration as string; parsed in Remember middleware
|
||||
} `json:"session"`
|
||||
|
||||
// Site metadata provided to templates (branding/UI only)
|
||||
Site struct {
|
||||
SiteName string `json:"siteName"`
|
||||
CopyrightYearStart int `json:"copyrightYearStart"`
|
||||
|
||||
@@ -1,3 +1,40 @@
|
||||
// Package csrf
|
||||
// Path: /internal/platform/csrf
|
||||
// File: csrf.go
|
||||
//
|
||||
// Purpose
|
||||
//
|
||||
// Centralized CSRF protection wrapper using justinas/nosurf.
|
||||
// Applies default CSRF protections across the entire HTTP handler tree
|
||||
// after SCS session load/save wrapping.
|
||||
//
|
||||
// Responsibilities (as implemented here)
|
||||
// 1. Construct a nosurf middleware handler over the provided http.Handler.
|
||||
// 2. Configure the base CSRF cookie using values from the App configuration.
|
||||
// 3. Enforce HttpOnly and SameSite=Lax defaults.
|
||||
// 4. Enable Secure flag automatically in production mode.
|
||||
//
|
||||
// HTTP stack order (per bootstrap)
|
||||
//
|
||||
// Gin Router → SCS LoadAndSave → CSRF Wrapper → http.Server
|
||||
//
|
||||
// Design notes
|
||||
// - The nosurf package automatically:
|
||||
// - Inserts CSRF token into responses (e.g., via nosurf.Token(c.Request))
|
||||
// - Validates token on state-changing requests (POST, PUT, etc.)
|
||||
// - CSRF cookie name is configurable via config.Config.
|
||||
// - Secure flag is tied to cfg.HttpServer.ProductionMode (recommended).
|
||||
// - Global protection: all routed POSTs are covered automatically.
|
||||
//
|
||||
// TODOs (observations from current implementation)
|
||||
// - Expose helper to fetch token into Gin templates via context key.
|
||||
// - Consider SameSiteStrictMode once OAuth/external logins are defined.
|
||||
// - Add domain and MaxAge settings for more precise control.
|
||||
// - Provide per-route opt-outs if needed for webhook endpoints.
|
||||
//
|
||||
// Change log
|
||||
//
|
||||
// [2025-10-29] Documentation updated to reflect middleware position and cookie policy.
|
||||
package csrf
|
||||
|
||||
import (
|
||||
@@ -8,6 +45,11 @@ import (
|
||||
"github.com/justinas/nosurf"
|
||||
)
|
||||
|
||||
// Wrap applies nosurf CSRF middleware to the given handler,
|
||||
// configuring the CSRF cookie based on App configuration.
|
||||
//
|
||||
// Caller must ensure this is positioned *outside* SCS LoadAndSave
|
||||
// so CSRF can access session data when generating/validating tokens.
|
||||
func Wrap(h http.Handler, cfg config.Config) http.Handler {
|
||||
cs := nosurf.New(h)
|
||||
cs.SetBaseCookie(http.Cookie{
|
||||
|
||||
@@ -1,3 +1,39 @@
|
||||
// Package databasePlatform
|
||||
// Path: /internal/platform/database
|
||||
// File: schema.go
|
||||
//
|
||||
// Purpose
|
||||
// Bootstrap and verify the initial application schema for MySQL using
|
||||
// embedded SQL. Applies the full schema only when the target database
|
||||
// is detected as "empty" via a probe query.
|
||||
//
|
||||
// Responsibilities (as implemented here)
|
||||
// 1) Detect whether the schema has been initialized by probing the users table.
|
||||
// 2) If empty, apply the embedded initial schema inside a single transaction.
|
||||
// 3) Use helper ExecScript to execute multi-statement SQL safely.
|
||||
// 4) Fail fast with contextual errors on probe/apply failures.
|
||||
//
|
||||
// Idempotency strategy
|
||||
// - Uses migrationSQL.ProbeUsersTable to query a known table and count rows.
|
||||
// - If count > 0, assumes schema exists and exits without applying SQL.
|
||||
// - This makes startup safe to repeat across restarts.
|
||||
//
|
||||
// Design notes
|
||||
// - InitialSchema is embedded (no external SQL files at runtime).
|
||||
// - Application is all-or-nothing via a single transaction.
|
||||
// - Console prints currently provide debug visibility during boot.
|
||||
// - Probe focuses on the "users" table as the presence indicator.
|
||||
//
|
||||
// TODOs (observations from current implementation)
|
||||
// - Replace debug prints with structured logging (levelled).
|
||||
// - Consider probing for table existence rather than row count to avoid
|
||||
// the edge case where users table exists but has zero rows.
|
||||
// - Introduce a schema version table for forward migrations.
|
||||
// - Expand error context to include which statement failed in ExecScript.
|
||||
//
|
||||
// Change log
|
||||
// [2025-10-29] Documentation aligned with embedded migrations and probe logic.
|
||||
|
||||
package databasePlatform
|
||||
|
||||
import (
|
||||
@@ -8,9 +44,12 @@ import (
|
||||
migrationSQL "synlotto-website/internal/storage/migrations"
|
||||
)
|
||||
|
||||
// EnsureInitialSchema ensures the database contains the baseline schema.
|
||||
// If the probe indicates an existing install, the function is a no-op.
|
||||
func EnsureInitialSchema(db *sql.DB) error {
|
||||
fmt.Println("✅ EnsureInitialSchema called") // temp debug
|
||||
|
||||
// Probe: if users table exists & has rows, treat schema as present.
|
||||
var cnt int
|
||||
if err := db.QueryRow(migrationSQL.ProbeUsersTable).Scan(&cnt); err != nil {
|
||||
return fmt.Errorf("probe users table failed: %w", err)
|
||||
@@ -20,9 +59,10 @@ func EnsureInitialSchema(db *sql.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// sanity: show embedded SQL length so we know it actually embedded
|
||||
// Sanity: visibility for embedded SQL payload size.
|
||||
fmt.Printf("📦 Initial SQL bytes: %d\n", len(migrationSQL.InitialSchema)) // temp debug
|
||||
|
||||
// Apply full schema atomically.
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,3 +1,35 @@
|
||||
// Package session
|
||||
// Path: /internal/platform/session
|
||||
// File: session.go
|
||||
//
|
||||
// Purpose
|
||||
// Initialize and configure the SCS (Server-Side Sessions) session manager
|
||||
// based on application configuration. Controls session lifetime, idle timeout,
|
||||
// cookie policy, and security posture.
|
||||
//
|
||||
// Responsibilities (as implemented here)
|
||||
// 1) Create SCS session manager used globally via bootstrap.
|
||||
// 2) Parse session lifetime + idle timeout from configuration.
|
||||
// 3) Apply secure cookie settings (HttpOnly, SameSite, Secure if production).
|
||||
// 4) Provide sensible defaults if configuration is invalid.
|
||||
//
|
||||
// Design notes
|
||||
// - SCS stores session data server-side (DB, file, mem, etc. — backend not set here).
|
||||
// - Cookie lifespan is enforced server-side (not just client expiry).
|
||||
// - Secure flag toggled via cfg.HttpServer.ProductionMode.
|
||||
// - Defaults keep application functional even if config is incomplete.
|
||||
//
|
||||
// TODOs (observations from current implementation)
|
||||
// - Add structured validation + error logging for invalid duration strings.
|
||||
// - Move secure cookie flag to config for more granular environment control.
|
||||
// - Consider enabling:
|
||||
// • Cookie.Persist (for "keep me logged in" flows)
|
||||
// • Cookie.SameSite = StrictMode by default
|
||||
// - Potentially expose SCS store configuration here (DB-backed sessions).
|
||||
//
|
||||
// Change log
|
||||
// [2025-10-29] Documentation aligned with final session architecture.
|
||||
|
||||
package session
|
||||
|
||||
import (
|
||||
@@ -9,6 +41,8 @@ import (
|
||||
"github.com/alexedwards/scs/v2"
|
||||
)
|
||||
|
||||
// New constructs a new SCS SessionManager using values from Config,
|
||||
// falling back to secure defaults if configuration is missing/invalid.
|
||||
func New(cfg config.Config) *scs.SessionManager {
|
||||
s := scs.New()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user