Files
Tomas Dvorak 355a97bab4 overhaul
2026-04-14 18:04:48 +02:00

273 lines
9.6 KiB
Go

package config
import (
"fmt"
"net/url"
"os"
"strconv"
"strings"
"time"
)
// Config represents the application configuration
type Config struct {
// Server Configuration
Port int `env:"PORT" default:"8080"`
Host string `env:"HOST" default:"localhost"`
ReadTimeout time.Duration `env:"READ_TIMEOUT" default:"30s"`
WriteTimeout time.Duration `env:"WRITE_TIMEOUT" default:"30s"`
IdleTimeout time.Duration `env:"IDLE_TIMEOUT" default:"60s"`
ShutdownTimeout time.Duration `env:"SHUTDOWN_TIMEOUT" default:"30s"`
// Database Configuration
DatabaseURL string `env:"DATABASE_URL" default:"postgres://user:password@localhost/containr?sslmode=disable"`
SQLitePath string `env:"SQLITE_PATH" default:"./data/apwhy.db"`
MaxConnections int `env:"MAX_CONNECTIONS" default:"25"`
MaxIdleConnections int `env:"MAX_IDLE_CONNECTIONS" default:"5"`
ConnMaxLifetime time.Duration `env:"CONN_MAX_LIFETIME" default:"5m"`
// Security Configuration
JWTSecret string `env:"JWT_SECRET" default:"your-secret-key-change-in-production"`
SessionAccessCookie string `env:"SESSION_ACCESS_COOKIE" default:"access_token"`
SessionRefreshCookie string `env:"SESSION_REFRESH_COOKIE" default:"refresh_token"`
AccessTokenTTL time.Duration `env:"ACCESS_TOKEN_TTL" default:"15m"`
RefreshTokenTTL time.Duration `env:"REFRESH_TOKEN_TTL" default:"7d"`
BcryptCost int `env:"BCRYPT_COST" default:"12"`
// CORS Configuration
CORSOrigins []string `env:"CORS_ORIGINS" default:"*"`
CORSMethods []string `env:"CORS_METHODS" default:"GET,POST,PUT,PATCH,DELETE,OPTIONS"`
CORSHeaders []string `env:"CORS_HEADERS" default:"Origin,Content-Type,Accept,Authorization"`
CORSCredentials bool `env:"CORS_CREDENTIALS" default:"true"`
// API Gateway Configuration
APIKeyHeader string `env:"API_KEY_HEADER" default:"X-API-Key"`
ServiceTokenHeader string `env:"SERVICE_TOKEN_HEADER" default:"X-Service-Token"`
AllowRootRoutePrefix bool `env:"ALLOW_ROOT_ROUTE_PREFIX" default:"false"`
DefaultServiceTimeout time.Duration `env:"DEFAULT_SERVICE_TIMEOUT" default:"30s"`
// Rate Limiting Configuration
FreeRPM int `env:"FREE_RPM" default:"60"`
ProRPM int `env:"PRO_RPM" default:"600"`
BusinessRPM int `env:"BUSINESS_RPM" default:"3000"`
FreeMonthlyQuota int `env:"FREE_MONTHLY_QUOTA" default:"1000"`
ProMonthlyQuota int `env:"PRO_MONTHLY_QUOTA" default:"50000"`
BusinessMonthlyQuota int `env:"BUSINESS_MONTHLY_QUOTA" default:"300000"`
// Cookie Configuration
CookieSecure bool `env:"COOKIE_SECURE" default:"false"`
CookieDomain string `env:"COOKIE_DOMAIN" default:""`
CookiePath string `env:"COOKIE_PATH" default:"/"`
CookieSameSite string `env:"COOKIE_SAME_SITE" default:"lax"`
// Analytics Configuration
UmamiBaseURL string `env:"UMAMI_BASE_URL" default:""`
UmamiAPIKey string `env:"UMAMI_API_KEY" default:""`
UmamiWebsiteID string `env:"UMAMI_WEBSITE_ID" default:""`
// Logging Configuration
LogLevel string `env:"LOG_LEVEL" default:"info"`
LogFormat string `env:"LOG_FORMAT" default:"json"`
LogOutput string `env:"LOG_OUTPUT" default:"stdout"`
// Development/Debug Configuration
Debug bool `env:"DEBUG" default:"false"`
TrustedProxyCIDR string `env:"TRUSTED_PROXY_CIDR" default:""`
// Dashboard UI Configuration
DashboardUIBasePath string `env:"DASHBOARD_UI_BASE_PATH" default:"/"`
}
// Environment represents the application environment
type Environment string
const (
Development Environment = "development"
Production Environment = "production"
Staging Environment = "staging"
Test Environment = "test"
)
// Load loads configuration from environment variables with defaults
func Load() *Config {
cfg := &Config{}
// Use reflection or manual field setting for simplicity
cfg.Port = getenvInt("PORT", 8080)
cfg.Host = getenv("HOST", "localhost")
cfg.ReadTimeout = getenvDuration("READ_TIMEOUT", 30*time.Second)
cfg.WriteTimeout = getenvDuration("WRITE_TIMEOUT", 30*time.Second)
cfg.IdleTimeout = getenvDuration("IDLE_TIMEOUT", 60*time.Second)
cfg.ShutdownTimeout = getenvDuration("SHUTDOWN_TIMEOUT", 30*time.Second)
cfg.DatabaseURL = encodeDatabasePassword(getenv("DATABASE_URL", "postgres://user:password@localhost/containr?sslmode=disable"))
cfg.SQLitePath = getenv("SQLITE_PATH", "./data/apwhy.db")
cfg.MaxConnections = getenvInt("MAX_CONNECTIONS", 25)
cfg.MaxIdleConnections = getenvInt("MAX_IDLE_CONNECTIONS", 5)
cfg.ConnMaxLifetime = getenvDuration("CONN_MAX_LIFETIME", 5*time.Minute)
cfg.JWTSecret = getenv("JWT_SECRET", "your-secret-key-change-in-production")
cfg.SessionAccessCookie = getenv("SESSION_ACCESS_COOKIE", "access_token")
cfg.SessionRefreshCookie = getenv("SESSION_REFRESH_COOKIE", "refresh_token")
cfg.AccessTokenTTL = getenvDuration("ACCESS_TOKEN_TTL", 15*time.Minute)
cfg.RefreshTokenTTL = getenvDuration("REFRESH_TOKEN_TTL", 7*24*time.Hour)
cfg.BcryptCost = getenvInt("BCRYPT_COST", 12)
cfg.CORSOrigins = getenvSlice("CORS_ORIGINS", []string{"*"})
cfg.CORSMethods = getenvSlice("CORS_METHODS", []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"})
cfg.CORSHeaders = getenvSlice("CORS_HEADERS", []string{"Origin", "Content-Type", "Accept", "Authorization"})
cfg.CORSCredentials = getenvBool("CORS_CREDENTIALS", true)
cfg.APIKeyHeader = getenv("API_KEY_HEADER", "X-API-Key")
cfg.ServiceTokenHeader = getenv("SERVICE_TOKEN_HEADER", "X-Service-Token")
cfg.AllowRootRoutePrefix = getenvBool("ALLOW_ROOT_ROUTE_PREFIX", false)
cfg.DefaultServiceTimeout = getenvDuration("DEFAULT_SERVICE_TIMEOUT", 30*time.Second)
cfg.FreeRPM = getenvInt("FREE_RPM", 60)
cfg.ProRPM = getenvInt("PRO_RPM", 600)
cfg.BusinessRPM = getenvInt("BUSINESS_RPM", 3000)
cfg.FreeMonthlyQuota = getenvInt("FREE_MONTHLY_QUOTA", 1000)
cfg.ProMonthlyQuota = getenvInt("PRO_MONTHLY_QUOTA", 50000)
cfg.BusinessMonthlyQuota = getenvInt("BUSINESS_MONTHLY_QUOTA", 300000)
cfg.CookieSecure = getenvBool("COOKIE_SECURE", false)
cfg.CookieDomain = getenv("COOKIE_DOMAIN", "")
cfg.CookiePath = getenv("COOKIE_PATH", "/")
cfg.CookieSameSite = getenv("COOKIE_SAME_SITE", "lax")
cfg.UmamiBaseURL = getenv("UMAMI_BASE_URL", "")
cfg.UmamiAPIKey = getenv("UMAMI_API_KEY", "")
cfg.UmamiWebsiteID = getenv("UMAMI_WEBSITE_ID", "")
cfg.LogLevel = getenv("LOG_LEVEL", "info")
cfg.LogFormat = getenv("LOG_FORMAT", "json")
cfg.LogOutput = getenv("LOG_OUTPUT", "stdout")
cfg.Debug = getenvBool("DEBUG", false)
cfg.TrustedProxyCIDR = getenv("TRUSTED_PROXY_CIDR", "")
cfg.DashboardUIBasePath = getenv("DASHBOARD_UI_BASE_PATH", "/")
// Validate configuration
if err := cfg.Validate(); err != nil {
panic(fmt.Sprintf("Invalid configuration: %v", err))
}
return cfg
}
// Validate validates the configuration
func (c *Config) Validate() error {
if c.JWTSecret == "your-secret-key-change-in-production" && c.GetEnvironment() == Production {
return fmt.Errorf("JWT_SECRET must be set in production")
}
if c.DatabaseURL == "" {
return fmt.Errorf("DATABASE_URL is required")
}
if c.Port < 1 || c.Port > 65535 {
return fmt.Errorf("PORT must be between 1 and 65535")
}
if c.BcryptCost < 10 || c.BcryptCost > 31 {
return fmt.Errorf("BCRYPT_COST must be between 10 and 31")
}
return nil
}
// GetEnvironment determines the current environment
func (c *Config) GetEnvironment() Environment {
env := strings.ToLower(getenv("ENVIRONMENT", "development"))
switch env {
case "production":
return Production
case "staging":
return Staging
case "test":
return Test
default:
return Development
}
}
// IsProduction returns true if running in production
func (c *Config) IsProduction() bool {
return c.GetEnvironment() == Production
}
// IsDevelopment returns true if running in development
func (c *Config) IsDevelopment() bool {
return c.GetEnvironment() == Development
}
// Helper functions for environment variable parsing
func getenv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getenvInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if intValue, err := strconv.Atoi(value); err == nil {
return intValue
}
}
return defaultValue
}
func getenvBool(key string, defaultValue bool) bool {
if value := os.Getenv(key); value != "" {
if boolValue, err := strconv.ParseBool(value); err == nil {
return boolValue
}
}
return defaultValue
}
func getenvDuration(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if duration, err := time.ParseDuration(value); err == nil {
return duration
}
}
return defaultValue
}
func getenvSlice(key string, defaultValue []string) []string {
if value := os.Getenv(key); value != "" {
return strings.Split(value, ",")
}
return defaultValue
}
// encodeDatabasePassword URL-encodes the password in a database connection string
func encodeDatabasePassword(databaseURL string) string {
// Find the password part between :@ and encode it
atIndex := strings.LastIndex(databaseURL, "@")
if atIndex == -1 {
return databaseURL // No @ found, return as is
}
beforeAt := databaseURL[:atIndex]
afterAt := databaseURL[atIndex:]
// Find the last : before the @ to locate the password
lastColonIndex := strings.LastIndex(beforeAt, ":")
if lastColonIndex == -1 {
return databaseURL // No : found, return as is
}
userPart := beforeAt[:lastColonIndex]
password := beforeAt[lastColonIndex+1:]
// URL encode the password
encodedPassword := url.QueryEscape(password)
// Reconstruct the URL
return userPart + ":" + encodedPassword + afterAt
}