package main import ( "errors" "fmt" "net/url" "strings" "time" "gardomatic.kleiax.de/internal/api" "gardomatic.kleiax.de/internal/mailer" "gardomatic.kleiax.de/internal/platform/environment" "gardomatic.kleiax.de/internal/platform/validate" "gardomatic.kleiax.de/internal/storage" ) func configFromEnvironment() (api.Config, error) { var errs []error required := func(name string) string { value, err := environment.Required(name) if err != nil { errs = append(errs, err) } return value } integer := func(name string, fallback int) int { value, err := environment.Int(name, fallback) if err != nil { errs = append(errs, err) } return value } boolean := func(name string, fallback bool) bool { value, err := environment.Bool(name, fallback) if err != nil { errs = append(errs, err) } return value } duration := func(name string, fallback time.Duration) time.Duration { value, err := environment.Duration(name, fallback) if err != nil { errs = append(errs, err) } return value } floating := func(name string, fallback float64) float64 { value, err := environment.Float(name, fallback) if err != nil { errs = append(errs, err) } return value } cfg := api.Config{ Host: environment.String("GARDOMATIC_API_HOST", ""), Port: integer("GARDOMATIC_API_PORT", 4000), Env: environment.String("GARDOMATIC_ENV", "development"), WebBaseURL: environment.String("GARDOMATIC_WEB_BASE_URL", "http://localhost:4040"), DemoAccountEmail: strings.ToLower(strings.TrimSpace( environment.String("GARDOMATIC_DEMO_ACCOUNT_EMAIL", ""), )), DB: api.DatabaseConfig{ Dsn: required("GARDOMATIC_DB_DSN"), MaxOpenConns: integer("GARDOMATIC_DB_MAX_OPEN_CONNS", 25), MaxIdleConns: integer("GARDOMATIC_DB_MAX_IDLE_CONNS", 25), MaxIdleTime: duration("GARDOMATIC_DB_MAX_IDLE_TIME", 15*time.Minute), }, Limiter: api.LimiterConfig{Enabled: boolean("GARDOMATIC_RATE_LIMIT_ENABLED", true), Rps: floating("GARDOMATIC_RATE_LIMIT_RPS", 10), Burst: integer("GARDOMATIC_RATE_LIMIT_BURST", 40)}, Session: api.SessionConfig{Lifetime: duration("GARDOMATIC_SESSION_LIFETIME", 12*time.Hour), IdleTimeout: duration("GARDOMATIC_SESSION_IDLE_TIMEOUT", 30*time.Minute), CookieName: environment.String("GARDOMATIC_SESSION_COOKIE_NAME", "gardomatic_session"), CookieSecure: boolean("GARDOMATIC_COOKIE_SECURE", false)}, Mail: mailer.Config{Mode: mailer.Mode(environment.String("GARDOMATIC_SMTP_MODE", string(mailer.ModeFile))), Host: environment.String("GARDOMATIC_SMTP_HOST", ""), Port: integer("GARDOMATIC_SMTP_PORT", 25), Username: environment.String("GARDOMATIC_SMTP_USERNAME", ""), Password: environment.String("GARDOMATIC_SMTP_PASSWORD", ""), Sender: environment.String("GARDOMATIC_SMTP_SENDER", "gardomatic@localhost"), FilePath: environment.String("GARDOMATIC_SMTP_FILE_PATH", "/tmp/gardomatic-mails.log")}, Cors: api.CORSConfig{TrustedOrigins: environment.CSV("GARDOMATIC_CORS_TRUSTED_ORIGINS")}, } if cfg.Port < 1 || cfg.Port > 65535 { errs = append(errs, errors.New("GARDOMATIC_API_PORT must be between 1 and 65535")) } if cfg.Env != "development" && cfg.Env != "test" && cfg.Env != "production" { errs = append(errs, fmt.Errorf("GARDOMATIC_ENV has unsupported value %q", cfg.Env)) } if cfg.DB.MaxOpenConns < 1 || cfg.DB.MaxIdleConns < 0 || cfg.DB.MaxIdleConns > cfg.DB.MaxOpenConns { errs = append(errs, errors.New("database pool limits are invalid")) } if cfg.Limiter.Rps <= 0 || cfg.Limiter.Burst < 1 { errs = append(errs, errors.New("rate limit values must be positive")) } if strings.TrimSpace(cfg.Session.CookieName) == "" { errs = append(errs, errors.New("GARDOMATIC_SESSION_COOKIE_NAME must not be empty")) } for _, origin := range cfg.Cors.TrustedOrigins { parsed, err := url.ParseRequestURI(origin) if err != nil || parsed.Scheme == "" || parsed.Host == "" { errs = append(errs, fmt.Errorf("invalid trusted origin %q", origin)) } } if parsed, err := url.ParseRequestURI(cfg.WebBaseURL); err != nil || parsed.Scheme == "" || parsed.Host == "" { errs = append(errs, errors.New("GARDOMATIC_WEB_BASE_URL must be an absolute URL")) } if cfg.Env == "production" && !cfg.Session.CookieSecure { errs = append(errs, errors.New("GARDOMATIC_COOKIE_SECURE must be true in production")) } if cfg.DemoAccountEmail != "" { v := validate.New() if storage.ValidateEmail(v, cfg.DemoAccountEmail); !v.Valid() { errs = append(errs, errors.New("GARDOMATIC_DEMO_ACCOUNT_EMAIL must be a valid email address")) } } if _, err := mailer.New(cfg.Mail); err != nil { errs = append(errs, fmt.Errorf("mail configuration: %w", err)) } return cfg, errors.Join(errs...) }