Code documentation
This commit is contained in:
223
README.md
223
README.md
@@ -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.
|
||||
@@ -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