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) }