// Package config loads and validates SoloPM's environment-variable configuration. package config import ( "fmt" "os" "strconv" "strings" ) // Config holds every environment-driven setting for the SoloPM server. type Config struct { DatabaseURL string HTTPPort string BaseURL string SessionSecret string UploadDir string MaxUploadMB int MeiliURL string MeiliKey string SMTPHost string SMTPPort string SMTPUser string SMTPPass string SMTPFrom string GitHubClientID string GitHubClientSecret string GiteaURL string GiteaClientID string GiteaClientSecret string CullAfterDays int LogLevel string } // SearchEnabled reports whether Meilisearch is configured. func (c *Config) SearchEnabled() bool { return c.MeiliURL != "" && c.MeiliKey != "" } // EmailEnabled reports whether SMTP is configured. func (c *Config) EmailEnabled() bool { return c.SMTPHost != "" } // GitHubOAuthEnabled reports whether GitHub OAuth is configured. func (c *Config) GitHubOAuthEnabled() bool { return c.GitHubClientID != "" && c.GitHubClientSecret != "" } // GiteaOAuthEnabled reports whether Gitea OAuth is configured. func (c *Config) GiteaOAuthEnabled() bool { return c.GiteaURL != "" && c.GiteaClientID != "" && c.GiteaClientSecret != "" } // Load reads configuration from the environment, applying defaults and // failing fast (returning an error) if a required variable is missing or // malformed. func Load() (*Config, error) { var missing []string required := func(key string) string { v := os.Getenv(key) if v == "" { missing = append(missing, key) } return v } c := &Config{ DatabaseURL: required("DATABASE_URL"), HTTPPort: getDefault("HTTP_PORT", "8080"), BaseURL: required("BASE_URL"), SessionSecret: required("SESSION_SECRET"), UploadDir: getDefault("UPLOAD_DIR", "./uploads"), MeiliURL: os.Getenv("MEILI_URL"), MeiliKey: os.Getenv("MEILI_KEY"), SMTPHost: os.Getenv("SMTP_HOST"), SMTPPort: os.Getenv("SMTP_PORT"), SMTPUser: os.Getenv("SMTP_USER"), SMTPPass: os.Getenv("SMTP_PASS"), SMTPFrom: os.Getenv("SMTP_FROM"), GitHubClientID: os.Getenv("GITHUB_CLIENT_ID"), GitHubClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"), GiteaURL: os.Getenv("GITEA_URL"), GiteaClientID: os.Getenv("GITEA_CLIENT_ID"), GiteaClientSecret: os.Getenv("GITEA_CLIENT_SECRET"), LogLevel: getDefault("LOG_LEVEL", "info"), } if len(missing) > 0 { return nil, fmt.Errorf("config: missing required environment variable(s): %s", strings.Join(missing, ", ")) } if len(c.SessionSecret) < 32 { return nil, fmt.Errorf("config: SESSION_SECRET must be at least 32 bytes, got %d", len(c.SessionSecret)) } maxUploadMB, err := getIntDefault("MAX_UPLOAD_MB", 25) if err != nil { return nil, err } c.MaxUploadMB = maxUploadMB cullAfterDays, err := getIntDefault("CULL_AFTER_DAYS", 30) if err != nil { return nil, err } c.CullAfterDays = cullAfterDays return c, nil } func getDefault(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } func getIntDefault(key string, def int) (int, error) { v := os.Getenv(key) if v == "" { return def, nil } n, err := strconv.Atoi(v) if err != nil { return 0, fmt.Errorf("config: %s must be an integer, got %q", key, v) } return n, nil }