package config import ( "errors" "fmt" "net" "os" "time" "github.com/spf13/viper" ) // Config holds the application configuration type Config struct { DatabaseURL string `mapstructure:"database_url"` // Primary database URL (used for writes, and reads if DatabaseReadURL not set) DatabaseReadURL string `mapstructure:"database_read_url"` // Optional: read-only database URL (falls back to DatabaseURL if not set) JWTSecret string `mapstructure:"jwt_secret"` JWTExpiration time.Duration `mapstructure:"jwt_expiration"` ServerPort int `mapstructure:"server_port"` ServerHost string `mapstructure:"server_host"` // IP address or hostname to bind to (mutually exclusive with server_interface) ServerInterface string `mapstructure:"server_interface"` // Network interface name to bind to (mutually exclusive with server_host) ArchiveStoragePath string `mapstructure:"archive_storage_path"` // Path to store archived files EnableAccessLog bool `mapstructure:"enable_access_log"` // Enable HTTP access logging middleware } // Load loads configuration from file or environment variables func Load(configPath string) (*Config, error) { v := viper.New() // Set defaults v.SetDefault("database_url", "sqlite://devdata/hako.db") v.SetDefault("server_port", 8080) v.SetDefault("jwt_expiration", "24h") v.SetDefault("archive_storage_path", "devdata/archives") v.SetDefault("enable_access_log", true) // Environment variables v.SetEnvPrefix("") v.AutomaticEnv() // Bind environment variables _ = v.BindEnv("database_url", "DATABASE_URL") _ = v.BindEnv("database_read_url", "DATABASE_READ_URL") _ = v.BindEnv("jwt_secret", "JWT_SECRET") _ = v.BindEnv("jwt_expiration", "JWT_EXPIRATION") _ = v.BindEnv("server_port", "SERVER_PORT") _ = v.BindEnv("server_host", "SERVER_HOST") _ = v.BindEnv("server_interface", "SERVER_INTERFACE") _ = v.BindEnv("archive_storage_path", "ARCHIVE_STORAGE_PATH") _ = v.BindEnv("enable_access_log", "ENABLE_ACCESS_LOG") // If config file is provided, read from it if configPath != "" { v.SetConfigFile(configPath) v.SetConfigType("yaml") // Default to yaml, but viper can auto-detect if err := v.ReadInConfig(); err != nil { // Allow missing config file (configuration can come from env vars) // but return error for other issues like permission denied if !errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("failed to read config file: %w", err) } // Config file not found, continue with env vars and defaults } } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } // Validate required fields if cfg.DatabaseURL == "" { return nil, fmt.Errorf("database_url is required") } // If DatabaseReadURL is not specified, use DatabaseURL for reads if cfg.DatabaseReadURL == "" { cfg.DatabaseReadURL = cfg.DatabaseURL } if cfg.JWTSecret == "" { return nil, fmt.Errorf("jwt_secret is required") } // Validate that server_host and server_interface are not both set if cfg.ServerHost != "" && cfg.ServerInterface != "" { return nil, fmt.Errorf("server_host and server_interface cannot be defined at the same time") } // Parse JWT expiration if it's a string if cfg.JWTExpiration == 0 { expStr := v.GetString("jwt_expiration") if expStr != "" { duration, err := time.ParseDuration(expStr) if err != nil { return nil, fmt.Errorf("invalid jwt_expiration format: %w", err) } cfg.JWTExpiration = duration } else { cfg.JWTExpiration = 24 * time.Hour // Default to 24 hours } } return &cfg, nil } // GetBindAddress returns the address to bind the server to (host:port or interface IP:port) func (c *Config) GetBindAddress() (string, error) { port := c.ServerPort if port == 0 { port = 8080 // Default port } var host string if c.ServerInterface != "" { // Get IP address from interface name iface, err := net.InterfaceByName(c.ServerInterface) if err != nil { return "", fmt.Errorf("failed to find interface %s: %w", c.ServerInterface, err) } addrs, err := iface.Addrs() if err != nil { return "", fmt.Errorf("failed to get addresses for interface %s: %w", c.ServerInterface, err) } // Find the first IPv4 address for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip != nil && ip.To4() != nil { host = ip.String() break } } if host == "" { return "", fmt.Errorf("no IPv4 address found on interface %s", c.ServerInterface) } } else if c.ServerHost != "" { host = c.ServerHost } else { // Default: bind to all interfaces host = "" } return fmt.Sprintf("%s:%d", host, port), nil } // GetServerURL returns the URL to access the server func (c *Config) GetServerURL() (string, error) { port := c.ServerPort if port == 0 { port = 8080 // Default port } var host string if c.ServerInterface != "" { // Get IP address from interface name iface, err := net.InterfaceByName(c.ServerInterface) if err != nil { return "", fmt.Errorf("failed to find interface %s: %w", c.ServerInterface, err) } addrs, err := iface.Addrs() if err != nil { return "", fmt.Errorf("failed to get addresses for interface %s: %w", c.ServerInterface, err) } // Find the first IPv4 address for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } if ip != nil && ip.To4() != nil { host = ip.String() break } } if host == "" { return "", fmt.Errorf("no IPv4 address found on interface %s", c.ServerInterface) } } else if c.ServerHost != "" { host = c.ServerHost } else { // Default: localhost host = "localhost" } return fmt.Sprintf("http://%s:%d", host, port), nil }