// Package config loads server configuration from environment variables, // optionally seeded from a .env file in the working directory. package config import ( "bufio" "fmt" "os" "strings" ) type Config struct { Port string PBURL string PBAdminEmail string PBAdminPasswd string CORSOrigins []string AuthSecret string UsersCollection string } // devAuthSecret is used only when AUTH_SECRET is unset, so the server still // runs out-of-the-box in development. Set AUTH_SECRET in production. const devAuthSecret = "dev-insecure-secret-change-me" // Load reads .env (if present) into the process environment, then builds a // Config from environment variables. Required values that are missing produce // an error so the server fails fast instead of misbehaving later. func Load() (*Config, error) { loadDotEnv(".env") cfg := &Config{ Port: getenv("PORT", "8080"), PBURL: strings.TrimRight(getenv("PB_URL", "http://10.2.1.10:8027"), "/"), PBAdminEmail: os.Getenv("PB_ADMIN_EMAIL"), PBAdminPasswd: os.Getenv("PB_ADMIN_PASSWORD"), CORSOrigins: splitCSV(getenv("CORS_ORIGINS", "http://localhost:5173")), AuthSecret: getenv("AUTH_SECRET", devAuthSecret), UsersCollection: getenv("AUTH_USERS_COLLECTION", "users"), } if cfg.PBAdminEmail == "" || cfg.PBAdminPasswd == "" { return nil, fmt.Errorf("PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD are required") } return cfg, nil } // UsingDevAuthSecret reports whether the insecure development secret is in use. func (c *Config) UsingDevAuthSecret() bool { return c.AuthSecret == devAuthSecret } func getenv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func splitCSV(s string) []string { var out []string for _, p := range strings.Split(s, ",") { if p = strings.TrimSpace(p); p != "" { out = append(out, p) } } return out } // loadDotEnv parses a simple KEY=VALUE file and sets any variables that are not // already present in the environment. Lines starting with # are comments. func loadDotEnv(path string) { f, err := os.Open(path) if err != nil { return // .env is optional } defer f.Close() sc := bufio.NewScanner(f) for sc.Scan() { line := strings.TrimSpace(sc.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } key, val, ok := strings.Cut(line, "=") if !ok { continue } key = strings.TrimSpace(key) val = strings.Trim(strings.TrimSpace(val), `"'`) if _, exists := os.LookupEnv(key); !exists { os.Setenv(key, val) } } }