385 lines
13 KiB
Go
385 lines
13 KiB
Go
package config
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// Config holds all application configuration
|
|
type Config struct {
|
|
Server ServerConfig `mapstructure:"server"`
|
|
Database DatabaseConfig `mapstructure:"database"`
|
|
App AppConfig `mapstructure:"app"`
|
|
Security SecurityConfig `mapstructure:"security"`
|
|
Logging LoggingConfig `mapstructure:"logging"`
|
|
External ExternalConfig `mapstructure:"external_services"`
|
|
Features FeatureConfig `mapstructure:"features"`
|
|
Defaults DefaultConfig `mapstructure:"defaults"`
|
|
}
|
|
|
|
// ServerConfig holds server-related configuration
|
|
type ServerConfig struct {
|
|
Host string `mapstructure:"host"`
|
|
Port int `mapstructure:"port"`
|
|
ReadTimeout time.Duration `mapstructure:"read_timeout"`
|
|
WriteTimeout time.Duration `mapstructure:"write_timeout"`
|
|
IdleTimeout time.Duration `mapstructure:"idle_timeout"`
|
|
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"`
|
|
}
|
|
|
|
// DatabaseConfig holds database-related configuration
|
|
type DatabaseConfig struct {
|
|
Path string `mapstructure:"path"`
|
|
ConnectionPool DatabaseConnectionConfig `mapstructure:"connection_pool"`
|
|
Logging DatabaseLoggingConfig `mapstructure:"logging"`
|
|
Migration DatabaseMigrationConfig `mapstructure:"migration"`
|
|
Performance DatabasePerformanceConfig `mapstructure:"performance"`
|
|
}
|
|
|
|
// DatabaseConnectionConfig holds database connection pool settings
|
|
type DatabaseConnectionConfig struct {
|
|
MaxIdleConnections int `mapstructure:"max_idle_connections"`
|
|
MaxOpenConnections int `mapstructure:"max_open_connections"`
|
|
ConnectionMaxLifetime time.Duration `mapstructure:"connection_max_lifetime"`
|
|
ConnectionMaxIdleTime time.Duration `mapstructure:"connection_max_idle_time"`
|
|
}
|
|
|
|
// DatabaseLoggingConfig holds database logging settings
|
|
type DatabaseLoggingConfig struct {
|
|
Level string `mapstructure:"level"`
|
|
SlowQueryThreshold time.Duration `mapstructure:"slow_query_threshold"`
|
|
Debug bool `mapstructure:"debug"`
|
|
}
|
|
|
|
// DatabaseMigrationConfig holds database migration settings
|
|
type DatabaseMigrationConfig struct {
|
|
AutoMigrate bool `mapstructure:"auto_migrate"`
|
|
DropTablesFirst bool `mapstructure:"drop_tables_first"`
|
|
CreateBatchSize int `mapstructure:"create_batch_size"`
|
|
}
|
|
|
|
// DatabasePerformanceConfig holds database performance settings
|
|
type DatabasePerformanceConfig struct {
|
|
PrepareStatements bool `mapstructure:"prepare_statements"`
|
|
DisableForeignKeyCheck bool `mapstructure:"disable_foreign_key_check"`
|
|
IgnoreRelationshipsWhenMigrating bool `mapstructure:"ignore_relationships_when_migrating"`
|
|
QueryFields bool `mapstructure:"query_fields"`
|
|
DryRun bool `mapstructure:"dry_run"`
|
|
CreateInBatches int `mapstructure:"create_in_batches"`
|
|
}
|
|
|
|
// AppConfig holds general application settings
|
|
type AppConfig struct {
|
|
Name string `mapstructure:"name"`
|
|
Version string `mapstructure:"version"`
|
|
Environment string `mapstructure:"environment"`
|
|
Debug bool `mapstructure:"debug"`
|
|
}
|
|
|
|
// SecurityConfig holds security-related settings
|
|
type SecurityConfig struct {
|
|
Session SessionConfig `mapstructure:"session"`
|
|
Password PasswordConfig `mapstructure:"password"`
|
|
}
|
|
|
|
// SessionConfig holds session management settings
|
|
type SessionConfig struct {
|
|
Timeout time.Duration `mapstructure:"timeout"`
|
|
CookieName string `mapstructure:"cookie_name"`
|
|
SecureCookies bool `mapstructure:"secure_cookies"`
|
|
HttpOnly bool `mapstructure:"http_only"`
|
|
}
|
|
|
|
// PasswordConfig holds password requirements
|
|
type PasswordConfig struct {
|
|
MinLength int `mapstructure:"min_length"`
|
|
RequireUppercase bool `mapstructure:"require_uppercase"`
|
|
RequireLowercase bool `mapstructure:"require_lowercase"`
|
|
RequireNumbers bool `mapstructure:"require_numbers"`
|
|
RequireSpecialChars bool `mapstructure:"require_special_chars"`
|
|
}
|
|
|
|
// LoggingConfig holds logging settings
|
|
type LoggingConfig struct {
|
|
Level string `mapstructure:"level"`
|
|
Format string `mapstructure:"format"`
|
|
Output string `mapstructure:"output"`
|
|
FilePath string `mapstructure:"file_path"`
|
|
Rotation LogRotationConfig `mapstructure:"rotation"`
|
|
}
|
|
|
|
// LogRotationConfig holds log rotation settings
|
|
type LogRotationConfig struct {
|
|
Enabled bool `mapstructure:"enabled"`
|
|
MaxSize string `mapstructure:"max_size"`
|
|
MaxAge string `mapstructure:"max_age"`
|
|
MaxBackups int `mapstructure:"max_backups"`
|
|
}
|
|
|
|
// ExternalConfig holds external service configurations
|
|
type ExternalConfig struct {
|
|
OverpassAPI OverpassAPIConfig `mapstructure:"overpass_api"`
|
|
}
|
|
|
|
// OverpassAPIConfig holds OpenStreetMap Overpass API settings
|
|
type OverpassAPIConfig struct {
|
|
URL string `mapstructure:"url"`
|
|
Timeout time.Duration `mapstructure:"timeout"`
|
|
MaxRetries int `mapstructure:"max_retries"`
|
|
SearchRadius int `mapstructure:"search_radius"`
|
|
}
|
|
|
|
// FeatureConfig holds feature flag settings
|
|
type FeatureConfig struct {
|
|
FuelStationSearch bool `mapstructure:"fuel_station_search"`
|
|
VehicleManagement bool `mapstructure:"vehicle_management"`
|
|
StatisticsDashboard bool `mapstructure:"statistics_dashboard"`
|
|
DataExport bool `mapstructure:"data_export"`
|
|
APIEndpoints bool `mapstructure:"api_endpoints"`
|
|
}
|
|
|
|
// DefaultConfig holds default values for new entities
|
|
type DefaultConfig struct {
|
|
Currency string `mapstructure:"currency"`
|
|
FuelType string `mapstructure:"fuel_type"`
|
|
DistanceUnit string `mapstructure:"distance_unit"`
|
|
VolumeUnit string `mapstructure:"volume_unit"`
|
|
}
|
|
|
|
// Load loads configuration from file, environment variables, and defaults
|
|
func Load(configPath string) (*Config, error) {
|
|
v := viper.New()
|
|
|
|
// Set defaults
|
|
setDefaults(v)
|
|
|
|
// Configure Viper
|
|
if configPath != "" {
|
|
v.SetConfigFile(configPath)
|
|
} else {
|
|
v.SetConfigName("config")
|
|
v.SetConfigType("yaml")
|
|
v.AddConfigPath(".")
|
|
v.AddConfigPath("./config")
|
|
v.AddConfigPath("$HOME/.tankstopp")
|
|
v.AddConfigPath("/etc/tankstopp")
|
|
}
|
|
|
|
// Enable environment variable binding
|
|
v.AutomaticEnv()
|
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
v.SetEnvPrefix("TANKSTOPP")
|
|
|
|
// Read config file
|
|
if err := v.ReadInConfig(); err != nil {
|
|
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
|
return nil, fmt.Errorf("error reading config file: %w", err)
|
|
}
|
|
// Config file not found, continue with defaults and env vars
|
|
}
|
|
|
|
// Unmarshal into struct
|
|
var config Config
|
|
if err := v.Unmarshal(&config); err != nil {
|
|
return nil, fmt.Errorf("error unmarshaling config: %w", err)
|
|
}
|
|
|
|
// Validate configuration
|
|
if err := config.Validate(); err != nil {
|
|
return nil, fmt.Errorf("invalid configuration: %w", err)
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
// setDefaults sets default values for configuration
|
|
func setDefaults(v *viper.Viper) {
|
|
// Server defaults
|
|
v.SetDefault("server.host", "localhost")
|
|
v.SetDefault("server.port", 8081)
|
|
v.SetDefault("server.read_timeout", "30s")
|
|
v.SetDefault("server.write_timeout", "30s")
|
|
v.SetDefault("server.idle_timeout", "120s")
|
|
v.SetDefault("server.shutdown_timeout", "10s")
|
|
|
|
// Database defaults
|
|
v.SetDefault("database.path", "fuel_stops.db")
|
|
v.SetDefault("database.connection_pool.max_idle_connections", 10)
|
|
v.SetDefault("database.connection_pool.max_open_connections", 100)
|
|
v.SetDefault("database.connection_pool.connection_max_lifetime", "1h")
|
|
v.SetDefault("database.connection_pool.connection_max_idle_time", "30m")
|
|
v.SetDefault("database.logging.level", "warn")
|
|
v.SetDefault("database.logging.slow_query_threshold", "200ms")
|
|
v.SetDefault("database.logging.debug", false)
|
|
v.SetDefault("database.migration.auto_migrate", true)
|
|
v.SetDefault("database.migration.drop_tables_first", false)
|
|
v.SetDefault("database.migration.create_batch_size", 1000)
|
|
v.SetDefault("database.performance.prepare_statements", true)
|
|
v.SetDefault("database.performance.disable_foreign_key_check", false)
|
|
v.SetDefault("database.performance.ignore_relationships_when_migrating", false)
|
|
v.SetDefault("database.performance.query_fields", true)
|
|
v.SetDefault("database.performance.dry_run", false)
|
|
v.SetDefault("database.performance.create_in_batches", 100)
|
|
|
|
// App defaults
|
|
v.SetDefault("app.name", "TankStopp")
|
|
v.SetDefault("app.version", "1.0.0")
|
|
v.SetDefault("app.environment", "development")
|
|
v.SetDefault("app.debug", true)
|
|
|
|
// Security defaults
|
|
v.SetDefault("security.session.timeout", "24h")
|
|
v.SetDefault("security.session.cookie_name", "tankstopp_session")
|
|
v.SetDefault("security.session.secure_cookies", false)
|
|
v.SetDefault("security.session.http_only", true)
|
|
v.SetDefault("security.password.min_length", 8)
|
|
v.SetDefault("security.password.require_uppercase", true)
|
|
v.SetDefault("security.password.require_lowercase", true)
|
|
v.SetDefault("security.password.require_numbers", true)
|
|
v.SetDefault("security.password.require_special_chars", false)
|
|
|
|
// Logging defaults
|
|
v.SetDefault("logging.level", "info")
|
|
v.SetDefault("logging.format", "text")
|
|
v.SetDefault("logging.output", "stdout")
|
|
v.SetDefault("logging.file_path", "logs/tankstopp.log")
|
|
v.SetDefault("logging.rotation.enabled", false)
|
|
v.SetDefault("logging.rotation.max_size", "100MB")
|
|
v.SetDefault("logging.rotation.max_age", "30d")
|
|
v.SetDefault("logging.rotation.max_backups", 5)
|
|
|
|
// External services defaults
|
|
v.SetDefault("external_services.overpass_api.url", "https://overpass-api.de/api/interpreter")
|
|
v.SetDefault("external_services.overpass_api.timeout", "30s")
|
|
v.SetDefault("external_services.overpass_api.max_retries", 3)
|
|
v.SetDefault("external_services.overpass_api.search_radius", 5000)
|
|
|
|
// Feature flags defaults
|
|
v.SetDefault("features.fuel_station_search", true)
|
|
v.SetDefault("features.vehicle_management", true)
|
|
v.SetDefault("features.statistics_dashboard", true)
|
|
v.SetDefault("features.data_export", true)
|
|
v.SetDefault("features.api_endpoints", true)
|
|
|
|
// Default values
|
|
v.SetDefault("defaults.currency", "EUR")
|
|
v.SetDefault("defaults.fuel_type", "Super E5")
|
|
v.SetDefault("defaults.distance_unit", "km")
|
|
v.SetDefault("defaults.volume_unit", "liters")
|
|
}
|
|
|
|
// Validate validates the configuration
|
|
func (c *Config) Validate() error {
|
|
// Validate server config
|
|
if c.Server.Port < 1 || c.Server.Port > 65535 {
|
|
return fmt.Errorf("invalid server port: %d", c.Server.Port)
|
|
}
|
|
|
|
// Validate database config
|
|
if c.Database.Path == "" {
|
|
return fmt.Errorf("database path cannot be empty")
|
|
}
|
|
|
|
if c.Database.ConnectionPool.MaxIdleConnections < 0 {
|
|
return fmt.Errorf("max idle connections cannot be negative")
|
|
}
|
|
|
|
if c.Database.ConnectionPool.MaxOpenConnections < 0 {
|
|
return fmt.Errorf("max open connections cannot be negative")
|
|
}
|
|
|
|
// Validate app config
|
|
if c.App.Name == "" {
|
|
return fmt.Errorf("app name cannot be empty")
|
|
}
|
|
|
|
validEnvs := []string{"development", "production", "test"}
|
|
if !contains(validEnvs, c.App.Environment) {
|
|
return fmt.Errorf("invalid environment: %s (must be one of: %v)", c.App.Environment, validEnvs)
|
|
}
|
|
|
|
// Validate security config
|
|
if c.Security.Password.MinLength < 4 {
|
|
return fmt.Errorf("minimum password length cannot be less than 4")
|
|
}
|
|
|
|
// Validate logging config
|
|
validLogLevels := []string{"debug", "info", "warn", "error"}
|
|
if !contains(validLogLevels, c.Logging.Level) {
|
|
return fmt.Errorf("invalid log level: %s (must be one of: %v)", c.Logging.Level, validLogLevels)
|
|
}
|
|
|
|
validLogFormats := []string{"json", "text"}
|
|
if !contains(validLogFormats, c.Logging.Format) {
|
|
return fmt.Errorf("invalid log format: %s (must be one of: %v)", c.Logging.Format, validLogFormats)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsProduction returns true if the app is running in production environment
|
|
func (c *Config) IsProduction() bool {
|
|
return c.App.Environment == "production"
|
|
}
|
|
|
|
// IsDevelopment returns true if the app is running in development environment
|
|
func (c *Config) IsDevelopment() bool {
|
|
return c.App.Environment == "development"
|
|
}
|
|
|
|
// IsTest returns true if the app is running in test environment
|
|
func (c *Config) IsTest() bool {
|
|
return c.App.Environment == "test"
|
|
}
|
|
|
|
// GetServerAddress returns the server address in host:port format
|
|
func (c *Config) GetServerAddress() string {
|
|
return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
|
|
}
|
|
|
|
// String returns a string representation of the configuration (without sensitive data)
|
|
func (c *Config) String() string {
|
|
return fmt.Sprintf(`TankStopp Configuration:
|
|
Server: %s
|
|
Database: %s
|
|
Environment: %s
|
|
Debug: %t
|
|
Features: %+v`,
|
|
c.GetServerAddress(),
|
|
c.Database.Path,
|
|
c.App.Environment,
|
|
c.App.Debug,
|
|
c.Features)
|
|
}
|
|
|
|
// contains checks if a slice contains a string
|
|
func contains(slice []string, item string) bool {
|
|
for _, s := range slice {
|
|
if s == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetConfigFromEnv returns environment-specific configuration
|
|
func GetConfigFromEnv() string {
|
|
if configPath := os.Getenv("TANKSTOPP_CONFIG_PATH"); configPath != "" {
|
|
return configPath
|
|
}
|
|
|
|
env := os.Getenv("TANKSTOPP_ENV")
|
|
if env == "" {
|
|
env = os.Getenv("ENV")
|
|
}
|
|
if env == "" {
|
|
env = "development"
|
|
}
|
|
|
|
return fmt.Sprintf("config.%s.yaml", env)
|
|
}
|