package config import ( "fmt" "os" "strconv" "strings" "time" ) type Config struct { Database DatabaseConfig Server ServerConfig JWT JWTConfig SMTP SMTPConfig App AppConfig RateLimit RateLimitConfig LogDir string PIDDir string } type DatabaseConfig struct { Host string Port string User string Password string Name string SSLMode string } type ServerConfig struct { Port string Host string ReadTimeout time.Duration WriteTimeout time.Duration IdleTimeout time.Duration MaxHeaderBytes int EnableTLS bool TLSCertFile string TLSKeyFile string } type JWTConfig struct { Secret string Expiration int RefreshExpiration int Issuer string Audience string KeyRotation KeyRotationConfig } type KeyRotationConfig struct { Enabled bool CurrentKey string PreviousKey string KeyID string } type SMTPConfig struct { Host string Port int Username string Password string From string Timeout time.Duration } type AppConfig struct { BaseURL string Debug bool AdminEmail string BcryptCost int Title string } type RateLimitConfig struct { AuthLimit int GeneralLimit int HealthLimit int MetricsLimit int TrustProxyHeaders bool } func Load() (*Config, error) { config := &Config{ Database: DatabaseConfig{ Host: getEnv("DB_HOST", "localhost"), Port: getEnv("DB_PORT", "5432"), User: getEnv("DB_USER", "postgres"), Password: getEnv("DB_PASSWORD", ""), Name: getEnv("DB_NAME", "goyco"), SSLMode: getEnv("DB_SSLMODE", "disable"), }, Server: ServerConfig{ Port: getEnv("SERVER_PORT", "8080"), Host: getEnv("SERVER_HOST", "0.0.0.0"), ReadTimeout: time.Duration(getEnvAsInt("SERVER_READ_TIMEOUT", 30)) * time.Second, WriteTimeout: time.Duration(getEnvAsInt("SERVER_WRITE_TIMEOUT", 30)) * time.Second, IdleTimeout: time.Duration(getEnvAsInt("SERVER_IDLE_TIMEOUT", 120)) * time.Second, MaxHeaderBytes: getEnvAsInt("SERVER_MAX_HEADER_BYTES", 1<<20), EnableTLS: getEnvAsBool("SERVER_ENABLE_TLS", false), TLSCertFile: getEnv("SERVER_TLS_CERT_FILE", ""), TLSKeyFile: getEnv("SERVER_TLS_KEY_FILE", ""), }, JWT: JWTConfig{ Secret: getEnv("JWT_SECRET", "your-secret-key"), Expiration: getEnvAsInt("JWT_EXPIRATION", 1), RefreshExpiration: getEnvAsInt("JWT_REFRESH_EXPIRATION", 168), Issuer: getEnv("JWT_ISSUER", "goyco"), Audience: getEnv("JWT_AUDIENCE", "goyco-users"), KeyRotation: KeyRotationConfig{ Enabled: getEnvAsBool("JWT_KEY_ROTATION_ENABLED", false), CurrentKey: getEnv("JWT_CURRENT_KEY", ""), PreviousKey: getEnv("JWT_PREVIOUS_KEY", ""), KeyID: getEnv("JWT_KEY_ID", "default"), }, }, SMTP: SMTPConfig{ Host: getEnv("SMTP_HOST", ""), Port: getEnvAsInt("SMTP_PORT", 587), Username: getEnv("SMTP_USERNAME", ""), Password: getEnv("SMTP_PASSWORD", ""), From: getEnv("SMTP_FROM", ""), Timeout: time.Duration(getEnvAsInt("SMTP_TIMEOUT", 30)) * time.Second, }, App: AppConfig{ BaseURL: getEnv("APP_BASE_URL", ""), Debug: getEnvAsBool("DEBUG", false), AdminEmail: getEnv("ADMIN_EMAIL", ""), BcryptCost: getEnvAsInt("BCRYPT_COST", 10), Title: getEnv("TITLE", "Goyco"), }, RateLimit: RateLimitConfig{ AuthLimit: getEnvAsInt("RATE_LIMIT_AUTH", 5), GeneralLimit: getEnvAsInt("RATE_LIMIT_GENERAL", 100), HealthLimit: getEnvAsInt("RATE_LIMIT_HEALTH", 60), MetricsLimit: getEnvAsInt("RATE_LIMIT_METRICS", 10), TrustProxyHeaders: getEnvAsBool("RATE_LIMIT_TRUST_PROXY", false), }, LogDir: getEnv("LOG_DIR", "/var/log/"), PIDDir: getEnv("PID_DIR", "/run"), } if config.App.BaseURL == "" { config.App.BaseURL = fmt.Sprintf("http://%s:%s", config.Server.Host, config.Server.Port) } if config.Database.Password == "" { return nil, fmt.Errorf("DB_PASSWORD is required") } if strings.TrimSpace(config.SMTP.Host) == "" { return nil, fmt.Errorf("SMTP_HOST is required") } if config.SMTP.Port <= 0 { return nil, fmt.Errorf("SMTP_PORT must be greater than 0") } if strings.TrimSpace(config.SMTP.From) == "" { return nil, fmt.Errorf("SMTP_FROM is required") } if strings.TrimSpace(config.App.AdminEmail) == "" { return nil, fmt.Errorf("ADMIN_EMAIL is required") } if config.Server.EnableTLS { if strings.TrimSpace(config.Server.TLSCertFile) == "" { return nil, fmt.Errorf("SERVER_TLS_CERT_FILE is required when SERVER_ENABLE_TLS is true") } if strings.TrimSpace(config.Server.TLSKeyFile) == "" { return nil, fmt.Errorf("SERVER_TLS_KEY_FILE is required when SERVER_ENABLE_TLS is true") } } if err := validateJWTConfig(&config.JWT); err != nil { return nil, err } if err := validateAppConfig(&config.App); err != nil { return nil, err } return config, nil } func (c *Config) GetConnectionString() string { return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s client_encoding=UTF8", c.Database.Host, c.Database.Port, c.Database.User, c.Database.Password, c.Database.Name, c.Database.SSLMode, ) } func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } func getEnvAsInt(key string, defaultValue int) int { if value := os.Getenv(key); value != "" { if intValue, err := strconv.Atoi(value); err == nil { return intValue } } return defaultValue } func getEnvAsBool(key string, defaultValue bool) bool { if value := os.Getenv(key); value != "" { if boolValue, err := strconv.ParseBool(value); err == nil { return boolValue } } return defaultValue } func validateJWTConfig(jwt *JWTConfig) error { if err := validateJWTSecret(jwt.Secret); err != nil { return err } if strings.TrimSpace(jwt.Issuer) == "" { return fmt.Errorf("JWT_ISSUER is required and cannot be empty") } if strings.TrimSpace(jwt.Audience) == "" { return fmt.Errorf("JWT_AUDIENCE is required and cannot be empty") } if jwt.Expiration <= 0 { return fmt.Errorf("JWT_EXPIRATION must be greater than 0") } if jwt.RefreshExpiration <= 0 { return fmt.Errorf("JWT_REFRESH_EXPIRATION must be greater than 0") } if jwt.RefreshExpiration <= jwt.Expiration { return fmt.Errorf("JWT_REFRESH_EXPIRATION must be greater than JWT_EXPIRATION") } if jwt.KeyRotation.Enabled { if strings.TrimSpace(jwt.KeyRotation.CurrentKey) == "" { return fmt.Errorf("JWT_CURRENT_KEY is required when key rotation is enabled") } if err := validateJWTSecret(jwt.KeyRotation.CurrentKey); err != nil { return fmt.Errorf("JWT_CURRENT_KEY validation failed: %w", err) } if jwt.KeyRotation.PreviousKey != "" { if err := validateJWTSecret(jwt.KeyRotation.PreviousKey); err != nil { return fmt.Errorf("JWT_PREVIOUS_KEY validation failed: %w", err) } } if strings.TrimSpace(jwt.KeyRotation.KeyID) == "" { return fmt.Errorf("JWT_KEY_ID is required when key rotation is enabled") } } return nil } func validateJWTSecret(secret string) error { trimmed := strings.TrimSpace(secret) if trimmed == "" { return fmt.Errorf("JWT secret is required and cannot be empty") } invalidSecrets := []string{ "your-secret-key", "secret", "jwt-secret", "my-secret", "change-me", "default-secret", "123456", "password", "admin", "test", "development", "production", "staging", } for _, invalid := range invalidSecrets { if strings.EqualFold(trimmed, invalid) { return fmt.Errorf("JWT secret cannot be a placeholder value like %q - please set a secure, random secret", invalid) } } if len(trimmed) < 32 { return fmt.Errorf("JWT secret must be at least 32 characters long for security (current length: %d)", len(trimmed)) } return nil } func validateAppConfig(app *AppConfig) error { if app.BcryptCost < 10 { return fmt.Errorf("BCRYPT_COST must be at least 10 for security (current: %d)", app.BcryptCost) } if app.BcryptCost > 14 { return fmt.Errorf("BCRYPT_COST must be at most 14 to avoid performance issues (current: %d)", app.BcryptCost) } return nil }