package config import ( "errors" "fmt" "log" "os" "strconv" "time" "github.com/joho/godotenv" "gorm.io/gorm/logger" ) // DatabaseConfig contains the configuration data for the database connection type DatabaseConfig struct { Host string Port int User string Password string DBName string SSLMode string MaxIdleConns int // Maximum number of idle connections MaxOpenConns int // Maximum number of open connections MaxLifetime time.Duration // Maximum lifetime of a connection LogLevel logger.LogLevel } // DefaultDatabaseConfig returns a default configuration with sensible values func DefaultDatabaseConfig() DatabaseConfig { return DatabaseConfig{ Host: "localhost", Port: 5432, User: "timetracker", Password: "password", DBName: "timetracker", SSLMode: "disable", MaxIdleConns: 10, MaxOpenConns: 100, MaxLifetime: time.Hour, LogLevel: logger.Info, } } // JWTConfig represents the configuration for JWT authentication type JWTConfig struct { Secret string TokenDuration time.Duration KeyGenerate bool KeyDir string PrivKeyFile string PubKeyFile string KeyBits int } // Config represents the application configuration type Config struct { Database DatabaseConfig JWTConfig JWTConfig APIKey string } // LoadConfig loads configuration from environment variables and .env file func LoadConfig() (*Config, error) { // Try loading .env file, but don't fail if it doesn't exist _ = godotenv.Load() cfg := &Config{ Database: DefaultDatabaseConfig(), JWTConfig: JWTConfig{}, } // Load database configuration if err := loadDatabaseConfig(cfg); err != nil { return nil, fmt.Errorf("failed to load database config: %w", err) } // Load JWT configuration if err := loadJWTConfig(cfg); err != nil { return nil, fmt.Errorf("failed to load JWT config: %w", err) } // Load API key cfg.APIKey = getEnv("API_KEY", "") return cfg, nil } // loadJWTConfig loads JWT configuration from environment func loadJWTConfig(cfg *Config) error { cfg.JWTConfig.Secret = getEnv("JWT_SECRET", "") defaultDuration := 24 * time.Hour durationStr := getEnv("JWT_TOKEN_DURATION", defaultDuration.String()) duration, err := time.ParseDuration(durationStr) if err != nil { return fmt.Errorf("invalid JWT_TOKEN_DURATION: %w", err) } cfg.JWTConfig.TokenDuration = duration keyGenerateStr := getEnv("JWT_KEY_GENERATE", "true") keyGenerate, err := strconv.ParseBool(keyGenerateStr) if err != nil { return fmt.Errorf("invalid JWT_KEY_GENERATE: %w", err) } cfg.JWTConfig.KeyGenerate = keyGenerate cfg.JWTConfig.KeyDir = getEnv("JWT_KEY_DIR", "./keys") cfg.JWTConfig.PrivKeyFile = getEnv("JWT_PRIV_KEY_FILE", "jwt.key") cfg.JWTConfig.PubKeyFile = getEnv("JWT_PUB_KEY_FILE", "jwt.key.pub") keyBitsStr := getEnv("JWT_KEY_BITS", "2048") keyBits, err := strconv.Atoi(keyBitsStr) if err != nil { return fmt.Errorf("invalid JWT_KEY_BITS: %w", err) } cfg.JWTConfig.KeyBits = keyBits return nil } // loadDatabaseConfig loads database configuration from environment func loadDatabaseConfig(cfg *Config) error { // Required fields cfg.Database.Host = getEnv("DB_HOST", cfg.Database.Host) cfg.Database.User = getEnv("DB_USER", cfg.Database.User) cfg.Database.Password = getEnv("DB_PASSWORD", cfg.Database.Password) cfg.Database.DBName = getEnv("DB_NAME", cfg.Database.DBName) cfg.Database.SSLMode = getEnv("DB_SSLMODE", cfg.Database.SSLMode) // Optional fields with parsing if port := getEnv("DB_PORT", ""); port != "" { portInt, err := strconv.Atoi(port) if err != nil || portInt <= 0 { return errors.New("invalid DB_PORT value") } cfg.Database.Port = portInt } // Log level based on environment if os.Getenv("ENVIRONMENT") == "production" { cfg.Database.LogLevel = logger.Error } else { cfg.Database.LogLevel = logger.Info } // Validate required fields if cfg.Database.Host == "" || cfg.Database.User == "" || cfg.Database.Password == "" || cfg.Database.DBName == "" { return errors.New("missing required database configuration") } return nil } // getEnv gets an environment variable with fallback func getEnv(key, fallback string) string { if value, exists := os.LookupEnv(key); exists { return value } return fallback } // MustLoadConfig loads configuration or panics on failure func MustLoadConfig() *Config { cfg, err := LoadConfig() if err != nil { log.Fatalf("Failed to load config: %v", err) } return cfg }