Code documentation

This commit is contained in:
2025-10-29 08:36:10 +00:00
parent 8d2ce27a74
commit 244b882f11
7 changed files with 458 additions and 11 deletions

223
README.md
View File

@@ -0,0 +1,223 @@
# Platform Architecture & Tech Stack
Internal developer documentation for the SynLotto platform infrastructure, covering the core platform modules where comments were updated and maintained. This serves as the reference for how the runtime environment is constructed and how foundational systems interact.
> **Current as of: Oct 29, 2025**
---
## Platform Initialization Overview
At startup the platform initializes and wires the systems required for HTTP request routing, security, session management, and database persistence.
Boot sequence executed from bootstrap:
### Config Load
→ MySQL Connect + Validate
→ EnsureInitialSchema (Embedded SQL, idempotent)
→ Register gob types for session data
→ Initialize SessionManager (SCS)
→ Create Gin Router (Logging, static assets)
→ Inject *App into Gin context for handler access
→ Route Handler → SCS LoadAndSave wrapping
→ CSRF Wrapping (NoSurf)
→ http.Server construction (graceful shutdown capable)
Application boot from main.go:
### Initialize template helpers
→ Attach global middleware (Auth → Remember)
→ Register route groups (Home, Account, Admin, Syndicate, Statistics)
→ Start serving HTTP requests
→ Graceful shutdown on SIGINT/SIGTERM
## Platform Files & Responsibilities
***internal/platform/bootstrap/loader.go***
The **application kernel** constructor.
Creates and wires:
- Config (loaded externally)
- MySQL DB connection (with pooling + UTF8MB4 + UTC)
- Idempotent initial schema application
- SCS SessionManager
- Gin router with logging
- Static mount: /static → ./web/static
- App → Gin context injection (c.Set("app", app))
- Custom NoRoute/NoMethod/Recovery error pages
- Final HTTP handler wrapping: Gin → SCS → CSRF
Orchestrates: stability of middleware order, security primitives, and transport-level behavior.
***cmd/api/main.go***
Top-level runtime control.
- Initializes template helpers (session manager + site meta)
- Applies Auth and Remember middleware
- Registers route groups
- Starts server in goroutine
- Uses timed graceful shutdown
No business logic or boot infrastructure allowed here.
***internal/platform/config/types.go***
Strongly typed runtime settings including:
Config Sections:
- Database (server, pool settings, credentials)
- HTTP server settings
- Session lifetimes + cookie names
- CSRF cookie name
- External API licensing
- Site metadata
Durations are strings — validated and parsed in platform/session.
***internal/platform/config/load.go***
Loads JSON configuration into Config struct.
- Pure function
- No mutation of global state
- Errors propagate to bootstrap
***internal/platform/config/config.go***
Singleton wrapper for global configuration access.
- Init ensures config is assigned only once
- Get allows consumers to retrieve config object
Used sparingly — dependency injection via App is primary recommended path.
***internal/platform/session/session.go***
Creates and configures SCS session manager.
Configured behaviors:
- Absolute lifetime (default 12h if invalid config)
- Idle timeout enforcement
- Cookie security:
- - HttpOnly = true
- - SameSite = Lax
- - Secure = based on productionMode
Responsible only for platform session settings — not auth behavior or token rotation.
***internal/platform/csrf/csrf.go***
Applies NoSurf global CSRF protection.
- Cookie name from config
- HttpOnly always
- Secure cookie in production
- SameSite = Lax
- Wraps after SCS to access stored session data
Requires template integration for token distribution.
***internal/platform/database/schema.go***
Ensures base DB schema exists using embedded SQL.
Behavior:
- Probes users table
- If any rows exist → assume schema complete
- Otherwise → executes InitialSchema in a single TX
Future: schema versioning required for incremental changes.
## Tech Stack Summary
|Concern | Technology |
| ------ | ------ |
|Web Framework|Gin|
|Session Manager|SCS (server-side)|
|CSRF Protection|NoSurf|
|Database|MySQL|
|Migrations|Embedded SQL applied on startup|
|Templates|Go html/template|
|Static Files|Served via Gin from web/static|
|Authentication|Cookie-based session auth|
|Error Views|Custom 404, 405, Recovery|
|Config Source|JSON configuration file|
|Routing|Grouped per feature under internal/http/routes|
## Security Behavior Summary
|Protection| Current Status|
| ------ | ------ |
|CSRF enforced globally|Yes|
|Session cookies HttpOnly|Yes|
|Secure cookie in production|Yes|
|SameSite policy|Lax|
|Idle timeout enforcement|Enabled|
|Session rotation on login|Enabled|
|DB foreign keys|Enabled|
|Secrets managed via JSON config|Temporary measure|
Security improvements tracked separately.
## Architectural Rules
|Layer |May Access|Must Not Access|
| ------ | ------ | ------ |
|Platform|DB, Session, Config|Handlers, routes|
|Handlers|App, DB, SessionManager, helpers|Bootstrap|
|Template helpers|Pure logic only|DB, HTTP|
|Middleware|Session, App, routing|Template rendering|
|Error pages|No DB or session dependency|Bootstrap internals|
These boundaries are currently enforced in code.
## Known Technical Debt
- Duration parsing and validation improvements
- Environment variable support for secret fields
- CSRF token auto-injection in templates
- Versioned DB migrations
- Replace remaining global config reads
- Add structured logging for platform initialization
- Expanded session store options (persistent)
Documented in developer backlog for scheduling.

View File

@@ -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 package config
import ( import (

View File

@@ -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 package config
import ( import (
@@ -5,6 +36,11 @@ import (
"os" "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) { func Load(path string) (Config, error) {
var cfg Config var cfg Config

View File

@@ -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 package config
// Config represents all runtime configuration for the application.
// Loaded from JSON and passed into bootstrap for wiring platform components.
type Config struct { type Config struct {
// CSRF cookie naming and storage controls
CSRF struct { CSRF struct {
CookieName string `json:"cookieName"` CookieName string `json:"cookieName"`
} `json:"csrf"` } `json:"csrf"`
// Database connection settings + tuning
Database struct { Database struct {
Server string `json:"server"` Server string `json:"server"`
Port int `json:"port"` Port int `json:"port"`
DatabaseNamed string `json:"databaseName"` DatabaseName string `json:"databaseName"`
MaxOpenConnections int `json:"maxOpenConnections"` MaxOpenConnections int `json:"maxOpenConnections"` // optional tuning
MaxIdleConnections int `json:"maxIdleConnections"` MaxIdleConnections int `json:"maxIdleConnections"` // optional tuning
ConnectionMaxLifetime string `json:"connectionMaxLifetime"` ConnectionMaxLifetime string `json:"connectionMaxLifetime"` // duration as string, parsed in bootstrap
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"` // sensitive; consider environment secrets
} }
// HTTP server exposure and security toggles
HttpServer struct { HttpServer struct {
Port int `json:"port"` Port int `json:"port"`
Address string `json:"address"` Address string `json:"address"`
ProductionMode bool `json:"productionMode"` ProductionMode bool `json:"productionMode"` // controls Secure cookie flag
} `json:"httpServer"` } `json:"httpServer"`
// Remote licensing API service configuration
License struct { License struct {
APIURL string `json:"apiUrl"` APIURL string `json:"apiUrl"`
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"` // sensitive; consider environment secrets
} `json:"license"` } `json:"license"`
// Session (SCS) configuration: cookie names, lifetime + idle timeout
Session struct { Session struct {
CookieName string `json:"cookieName"` CookieName string `json:"cookieName"`
Lifetime string `json:"lifetime"` Lifetime string `json:"lifetime"` // duration as string; parsed in platform/session
IdleTimeout string `json:"idleTimeout"` IdleTimeout string `json:"idleTimeout"` // duration as string; parsed in platform/session
RememberCookieName string `json:"rememberCookieName"` RememberCookieName string `json:"rememberCookieName"`
RememberDuration string `json:"rememberDuration"` RememberDuration string `json:"rememberDuration"` // duration as string; parsed in Remember middleware
} `json:"session"` } `json:"session"`
// Site metadata provided to templates (branding/UI only)
Site struct { Site struct {
SiteName string `json:"siteName"` SiteName string `json:"siteName"`
CopyrightYearStart int `json:"copyrightYearStart"` CopyrightYearStart int `json:"copyrightYearStart"`

View File

@@ -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 package csrf
import ( import (
@@ -8,6 +45,11 @@ import (
"github.com/justinas/nosurf" "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 { func Wrap(h http.Handler, cfg config.Config) http.Handler {
cs := nosurf.New(h) cs := nosurf.New(h)
cs.SetBaseCookie(http.Cookie{ cs.SetBaseCookie(http.Cookie{

View File

@@ -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 package databasePlatform
import ( import (
@@ -8,9 +44,12 @@ import (
migrationSQL "synlotto-website/internal/storage/migrations" 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 { func EnsureInitialSchema(db *sql.DB) error {
fmt.Println("✅ EnsureInitialSchema called") // temp debug fmt.Println("✅ EnsureInitialSchema called") // temp debug
// Probe: if users table exists & has rows, treat schema as present.
var cnt int var cnt int
if err := db.QueryRow(migrationSQL.ProbeUsersTable).Scan(&cnt); err != nil { if err := db.QueryRow(migrationSQL.ProbeUsersTable).Scan(&cnt); err != nil {
return fmt.Errorf("probe users table failed: %w", err) return fmt.Errorf("probe users table failed: %w", err)
@@ -20,9 +59,10 @@ func EnsureInitialSchema(db *sql.DB) error {
return nil 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 fmt.Printf("📦 Initial SQL bytes: %d\n", len(migrationSQL.InitialSchema)) // temp debug
// Apply full schema atomically.
tx, err := db.Begin() tx, err := db.Begin()
if err != nil { if err != nil {
return err return err

View File

@@ -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 package session
import ( import (
@@ -9,6 +41,8 @@ import (
"github.com/alexedwards/scs/v2" "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 { func New(cfg config.Config) *scs.SessionManager {
s := scs.New() s := scs.New()