config.go raw

   1  package config
   2  
   3  import (
   4  	"fmt"
   5  	"os"
   6  	"strconv"
   7  	"strings"
   8  	"time"
   9  
  10  	"gopkg.in/yaml.v3"
  11  )
  12  
  13  type Config struct {
  14  	Server  ServerConfig  `yaml:"server"`
  15  	OAuth2  OAuth2Config  `yaml:"oauth2"`
  16  	Nostr   NostrConfig   `yaml:"nostr"`
  17  }
  18  
  19  type ServerConfig struct {
  20  	Port    int    `yaml:"port"`
  21  	Host    string `yaml:"host"`
  22  	BaseURL string `yaml:"base_url"`
  23  }
  24  
  25  func (s ServerConfig) Address() string {
  26  	host := s.Host
  27  	if host == "" {
  28  		host = "0.0.0.0"
  29  	}
  30  	port := s.Port
  31  	if port == 0 {
  32  		port = 8080
  33  	}
  34  	return fmt.Sprintf("%s:%d", host, port)
  35  }
  36  
  37  type OAuth2Config struct {
  38  	Clients []ClientConfig `yaml:"clients"`
  39  }
  40  
  41  type ClientConfig struct {
  42  	ClientID     string   `yaml:"client_id"`
  43  	ClientSecret string   `yaml:"client_secret"`
  44  	RedirectURIs []string `yaml:"redirect_uris"`
  45  }
  46  
  47  type NostrConfig struct {
  48  	ChallengeTTL   time.Duration `yaml:"challenge_ttl"`
  49  	FallbackRelays []string      `yaml:"fallback_relays"`
  50  }
  51  
  52  func Load(path string) (*Config, error) {
  53  	data, err := os.ReadFile(path)
  54  	if err != nil {
  55  		return nil, err
  56  	}
  57  
  58  	var cfg Config
  59  	if err := yaml.Unmarshal(data, &cfg); err != nil {
  60  		return nil, fmt.Errorf("failed to parse config: %w", err)
  61  	}
  62  
  63  	cfg.setDefaults()
  64  	return &cfg, nil
  65  }
  66  
  67  func FromEnv() *Config {
  68  	cfg := &Config{}
  69  
  70  	// Server config
  71  	if port := os.Getenv("PORT"); port != "" {
  72  		cfg.Server.Port, _ = strconv.Atoi(port)
  73  	}
  74  	cfg.Server.Host = os.Getenv("HOST")
  75  	cfg.Server.BaseURL = os.Getenv("BASE_URL")
  76  
  77  	// OAuth2 client config (single client from env)
  78  	clientID := os.Getenv("OAUTH2_CLIENT_ID")
  79  	clientSecret := os.Getenv("OAUTH2_CLIENT_SECRET")
  80  	redirectURIs := os.Getenv("OAUTH2_REDIRECT_URIS")
  81  
  82  	if clientID != "" {
  83  		cfg.OAuth2.Clients = []ClientConfig{{
  84  			ClientID:     clientID,
  85  			ClientSecret: clientSecret,
  86  			RedirectURIs: strings.Split(redirectURIs, ","),
  87  		}}
  88  	}
  89  
  90  	// Nostr config
  91  	if ttl := os.Getenv("NOSTR_CHALLENGE_TTL"); ttl != "" {
  92  		cfg.Nostr.ChallengeTTL, _ = time.ParseDuration(ttl)
  93  	}
  94  	if relays := os.Getenv("NOSTR_FALLBACK_RELAYS"); relays != "" {
  95  		cfg.Nostr.FallbackRelays = strings.Split(relays, ",")
  96  	}
  97  
  98  	cfg.setDefaults()
  99  	return cfg
 100  }
 101  
 102  // DefaultFallbackRelays are well-known relays that aggregate profile data
 103  var DefaultFallbackRelays = []string{
 104  	"wss://relay.nostr.band/",
 105  	"wss://nostr.wine/",
 106  	"wss://nos.lol/",
 107  	"wss://relay.primal.net/",
 108  	"wss://purplepag.es/",
 109  }
 110  
 111  func (c *Config) setDefaults() {
 112  	if c.Server.Port == 0 {
 113  		c.Server.Port = 8080
 114  	}
 115  	if c.Server.BaseURL == "" {
 116  		c.Server.BaseURL = fmt.Sprintf("http://localhost:%d", c.Server.Port)
 117  	}
 118  	if c.Nostr.ChallengeTTL == 0 {
 119  		c.Nostr.ChallengeTTL = 60 * time.Second
 120  	}
 121  	if len(c.Nostr.FallbackRelays) == 0 {
 122  		c.Nostr.FallbackRelays = DefaultFallbackRelays
 123  	}
 124  }
 125  
 126  func (c *Config) GetClient(clientID string) *ClientConfig {
 127  	for i := range c.OAuth2.Clients {
 128  		if c.OAuth2.Clients[i].ClientID == clientID {
 129  			return &c.OAuth2.Clients[i]
 130  		}
 131  	}
 132  	return nil
 133  }
 134  
 135  func (c *Config) ValidateRedirectURI(clientID, redirectURI string) bool {
 136  	client := c.GetClient(clientID)
 137  	if client == nil {
 138  		return false
 139  	}
 140  	for _, uri := range client.RedirectURIs {
 141  		if uri == redirectURI {
 142  			return true
 143  		}
 144  	}
 145  	return false
 146  }
 147