package config import ( "errors" "fmt" "os" "path/filepath" "github.com/BurntSushi/toml" ) type Config struct { Server ServerConfig `toml:"server"` Security SecurityConfig `toml:"security"` Apify ApifyConfig `toml:"apify"` Ebay EbayConfig `toml:"ebay"` Ntfy NtfyConfig `toml:"ntfy"` Scheduler SchedulerConfig `toml:"scheduler"` } // EbayConfig holds credentials for eBay's official Buy > Browse API. When set, // eBay marketplaces are polled through the Browse API instead of an Apify // scraper actor. ClientID is the App ID and ClientSecret is the Cert ID from // the eBay developer keyset. Environment is "production" (default) or // "sandbox". Like the Apify key, both credentials can be overridden at // runtime via the Settings page. type EbayConfig struct { ClientID string `toml:"client_id"` ClientSecret string `toml:"client_secret"` Environment string `toml:"environment"` // DailyCallLimit caps Browse API calls per day, on eBay's own quota // clock (midnight US Pacific). Once reached, eBay polling halts until // the next reset. Defaults to 5000 (the standard Browse API allowance). // Set to a negative value to disable the cap. DailyCallLimit int `toml:"daily_call_limit"` } type ServerConfig struct { Port int `toml:"port"` DBPath string `toml:"db_path"` // SecureCookies sets the Secure attribute on the session cookie. It must // be true in any deployment reachable over HTTPS — including behind a // TLS-terminating proxy like Traefik, where the browser-facing leg is // HTTPS even though Veola itself speaks plain HTTP. Defaults to true; // set false only for local non-TLS development. SecureCookies *bool `toml:"secure_cookies"` } // UseSecureCookies resolves the SecureCookies setting, defaulting to true when // the key is absent from config. func (c ServerConfig) UseSecureCookies() bool { return c.SecureCookies == nil || *c.SecureCookies } type SecurityConfig struct { SessionSecret string `toml:"session_secret"` EncryptionKey string `toml:"encryption_key"` } type ApifyConfig struct { APIKey string `toml:"api_key"` Actors ActorConfig `toml:"actors"` Proxy ProxyConfig `toml:"proxy"` } // ProxyConfig controls the proxyConfiguration block passed to apify actors // that scrape sites which block datacenter IPs (e.g. eBay returns 403 without // a residential proxy). type ProxyConfig struct { UseApifyProxy bool `toml:"use_apify_proxy"` Groups []string `toml:"groups"` Country string `toml:"country"` } type ActorConfig struct { ActiveListings string `toml:"active_listings"` SoldListings string `toml:"sold_listings"` PriceComparison string `toml:"price_comparison"` YahooAuctionsJP string `toml:"yahoo_auctions_jp"` YahooAuctionsJPSold string `toml:"yahoo_auctions_jp_sold"` MercariJP string `toml:"mercari_jp"` } type NtfyConfig struct { BaseURL string `toml:"base_url"` DefaultTopic string `toml:"default_topic"` } type SchedulerConfig struct { GlobalPollIntervalMinutes int `toml:"global_poll_interval_minutes"` MatchConfidenceThreshold float64 `toml:"match_confidence_threshold"` } func Load(path string) (*Config, error) { if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { return nil, fmt.Errorf("config file not found at %s. Copy config.toml.example to that path and fill it in", path) } else if err != nil { return nil, fmt.Errorf("stat config: %w", err) } var c Config if _, err := toml.DecodeFile(path, &c); err != nil { return nil, fmt.Errorf("parse config: %w", err) } if err := c.validate(); err != nil { return nil, err } return &c, nil } func (c *Config) validate() error { if len(c.Security.SessionSecret) < 32 { return errors.New("security.session_secret must be at least 32 bytes") } if len(c.Security.EncryptionKey) < 32 { return errors.New("security.encryption_key must be at least 32 bytes") } if c.Security.SessionSecret == c.Security.EncryptionKey { return errors.New("security.session_secret and security.encryption_key must not be equal") } if c.Server.DBPath == "" { return errors.New("server.db_path must be set") } dir := filepath.Dir(c.Server.DBPath) if dir == "" { dir = "." } if err := checkWritable(dir); err != nil { return fmt.Errorf("server.db_path directory %s not writable: %w", dir, err) } if c.Server.Port == 0 { c.Server.Port = 8080 } if c.Scheduler.GlobalPollIntervalMinutes == 0 { c.Scheduler.GlobalPollIntervalMinutes = 60 } if c.Scheduler.MatchConfidenceThreshold == 0 { c.Scheduler.MatchConfidenceThreshold = 0.6 } if c.Ntfy.DefaultTopic == "" { c.Ntfy.DefaultTopic = "veola" } if c.Ebay.DailyCallLimit == 0 { c.Ebay.DailyCallLimit = 5000 } return nil } func checkWritable(dir string) error { f, err := os.CreateTemp(dir, ".veola-write-test-*") if err != nil { return err } name := f.Name() f.Close() return os.Remove(name) }