package config import ( "fmt" "os" "reflect" "strings" "time" "github.com/pelletier/go-toml/v2" mcdslauth "git.wntrmute.dev/mc/mcdsl/auth" ) // Config is the MCQ configuration. type Config struct { Server ServerConfig `toml:"server"` Database DatabaseConfig `toml:"database"` MCIAS mcdslauth.Config `toml:"mcias"` Log LogConfig `toml:"log"` } // ServerConfig holds HTTP/gRPC server settings. TLS fields are optional; // when empty, MCQ serves plain HTTP (for use behind mc-proxy L7). type ServerConfig struct { ListenAddr string `toml:"listen_addr"` GRPCAddr string `toml:"grpc_addr"` TLSCert string `toml:"tls_cert"` TLSKey string `toml:"tls_key"` ReadTimeout Duration `toml:"read_timeout"` WriteTimeout Duration `toml:"write_timeout"` IdleTimeout Duration `toml:"idle_timeout"` ShutdownTimeout Duration `toml:"shutdown_timeout"` } type DatabaseConfig struct { Path string `toml:"path"` } type LogConfig struct { Level string `toml:"level"` } // Duration wraps time.Duration for TOML string parsing. type Duration struct { time.Duration } func (d *Duration) UnmarshalText(text []byte) error { var err error d.Duration, err = time.ParseDuration(string(text)) return err } // Load reads, parses, and validates a config file. func Load(path string) (*Config, error) { data, err := os.ReadFile(path) // #nosec G304 -- config path is operator-controlled if err != nil { return nil, fmt.Errorf("read config: %w", err) } cfg := &Config{ Server: ServerConfig{ ListenAddr: ":8080", ReadTimeout: Duration{30 * time.Second}, WriteTimeout: Duration{30 * time.Second}, IdleTimeout: Duration{120 * time.Second}, ShutdownTimeout: Duration{60 * time.Second}, }, Log: LogConfig{Level: "info"}, } if err := toml.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("parse config: %w", err) } applyEnvOverrides(cfg) if err := cfg.validate(); err != nil { return nil, err } return cfg, nil } func (c *Config) validate() error { if c.Server.ListenAddr == "" { return fmt.Errorf("config: server.listen_addr is required") } if c.Database.Path == "" { return fmt.Errorf("config: database.path is required") } if c.MCIAS.ServerURL == "" { return fmt.Errorf("config: mcias.server_url is required") } return nil } func applyEnvOverrides(cfg *Config) { if port := os.Getenv("PORT"); port != "" { cfg.Server.ListenAddr = ":" + port } applyEnvToStruct("MCQ", reflect.ValueOf(cfg).Elem()) } func applyEnvToStruct(prefix string, v reflect.Value) { t := v.Type() for i := range t.NumField() { field := t.Field(i) fv := v.Field(i) tag := field.Tag.Get("toml") if tag == "" || tag == "-" { continue } envKey := prefix + "_" + strings.ToUpper(tag) if fv.Kind() == reflect.Struct && field.Type != reflect.TypeOf(Duration{}) { applyEnvToStruct(envKey, fv) continue } envVal := os.Getenv(envKey) if envVal == "" { continue } if fv.Kind() == reflect.String { fv.SetString(envVal) } if field.Type == reflect.TypeOf(Duration{}) { if d, err := time.ParseDuration(envVal); err == nil { fv.Set(reflect.ValueOf(Duration{d})) } } } }