package config import ( "fmt" "os" "time" "github.com/pelletier/go-toml/v2" ) type Config struct { Server ServerConfig `toml:"server"` Web WebConfig `toml:"web"` Database DatabaseConfig `toml:"database"` Auth AuthConfig `toml:"auth"` WebAuthn WebAuthnConfig `toml:"webauthn"` Log LogConfig `toml:"log"` } type ServerConfig struct { ListenAddr string `toml:"listen_addr"` GRPCAddr string `toml:"grpc_addr"` TLSCert string `toml:"tls_cert"` TLSKey string `toml:"tls_key"` } type WebConfig struct { ListenAddr string `toml:"listen_addr"` BaseURL string `toml:"base_url"` } type DatabaseConfig struct { Path string `toml:"path"` } type AuthConfig struct { TokenTTL string `toml:"token_ttl"` Argon2Memory uint32 `toml:"argon2_memory"` Argon2Time uint32 `toml:"argon2_time"` Argon2Threads uint8 `toml:"argon2_threads"` } func (a AuthConfig) TokenDuration() (time.Duration, error) { return time.ParseDuration(a.TokenTTL) } type WebAuthnConfig struct { RPDisplayName string `toml:"rp_display_name"` RPID string `toml:"rp_id"` RPOrigins []string `toml:"rp_origins"` } type LogConfig struct { Level string `toml:"level"` } func Load(path string) (*Config, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config: %w", err) } var cfg Config if err := toml.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parse config: %w", err) } if err := cfg.validate(); err != nil { return nil, fmt.Errorf("config validation: %w", err) } return &cfg, nil } func (c *Config) validate() error { if err := c.validateFields(); err != nil { return err } if err := c.validateFiles(); err != nil { return err } return nil } // validateFields checks config values that don't require filesystem access. func (c *Config) validateFields() error { if c.Database.Path == "" { return fmt.Errorf("database.path is required") } if c.Server.TLSCert == "" || c.Server.TLSKey == "" { return fmt.Errorf("server.tls_cert and server.tls_key are required") } if c.Server.ListenAddr == "" { return fmt.Errorf("server.listen_addr is required") } if c.Server.GRPCAddr == "" { return fmt.Errorf("server.grpc_addr is required") } d, err := c.Auth.TokenDuration() if err != nil { return fmt.Errorf("auth.token_ttl is invalid: %w", err) } if d <= 0 { return fmt.Errorf("auth.token_ttl must be positive") } if c.Auth.Argon2Memory == 0 { return fmt.Errorf("auth.argon2_memory must be greater than zero") } if c.Auth.Argon2Time == 0 { return fmt.Errorf("auth.argon2_time must be greater than zero") } if c.Auth.Argon2Threads == 0 { return fmt.Errorf("auth.argon2_threads must be greater than zero") } switch c.Log.Level { case "debug", "info", "warn", "error": // valid default: return fmt.Errorf("log.level must be one of: debug, info, warn, error (got %q)", c.Log.Level) } return nil } // validateFiles checks that referenced files exist on disk. func (c *Config) validateFiles() error { if _, err := os.Stat(c.Server.TLSCert); err != nil { return fmt.Errorf("server.tls_cert file not found: %w", err) } if _, err := os.Stat(c.Server.TLSKey); err != nil { return fmt.Errorf("server.tls_key file not found: %w", err) } return nil }