package config import ( "bufio" "log" "os" "path/filepath" "strconv" "strings" ) type Config struct { DatabaseURL string JWTSecret string RedisAddr string ServerPort string Env string BaseURL string CORSOrigins []string RateLimitRPS float64 RateLimitBurst int } func Load() *Config { loadEnvFromPath() port := getEnv("SERVER_PORT", "8080") jwtSecret := getEnv("JWT_SECRET", "dev-secret-change-me") env := getEnv("ENV", "development") if env == "production" && (jwtSecret == "" || jwtSecret == "dev-secret-change-me") { log.Fatal("JWT_SECRET must be set to a secure value in production") } corsOrigins := parseCORSOrigins(getEnv("CORS_ORIGINS", "http://localhost:5173,http://127.0.0.1:5173")) rateRPS := parseFloat(getEnv("RATE_LIMIT_RPS", "100"), 100) rateBurst := parseInt(getEnv("RATE_LIMIT_BURST", "200"), 200) return &Config{ DatabaseURL: getEnv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/calendarapi?sslmode=disable"), JWTSecret: jwtSecret, RedisAddr: os.Getenv("REDIS_ADDR"), ServerPort: port, Env: env, BaseURL: strings.TrimSuffix(getEnv("BASE_URL", "http://localhost:"+port), "/"), CORSOrigins: corsOrigins, RateLimitRPS: rateRPS, RateLimitBurst: rateBurst, } } func parseCORSOrigins(s string) []string { if s == "" { return []string{} } parts := strings.Split(s, ",") out := make([]string, 0, len(parts)) for _, p := range parts { p = strings.TrimSpace(p) if p != "" { out = append(out, p) } } if len(out) == 0 { return []string{"http://localhost:5173", "http://127.0.0.1:5173"} } return out } func parseFloat(s string, def float64) float64 { v, err := strconv.ParseFloat(s, 64) if err != nil { return def } return v } func parseInt(s string, def int) int { v, err := strconv.Atoi(s) if err != nil { return def } return v } func loadEnvFromPath() { if p := os.Getenv("ENV_FILE"); p != "" { loadEnvFile(p) return } cwd, _ := os.Getwd() for _, p := range []string{".env", "../.env"} { full := filepath.Join(cwd, p) if f, err := os.Open(full); err == nil { f.Close() loadEnvFile(full) return } } // Fallback: try current dir as-is (for relative paths) loadEnvFile(".env") } func loadEnvFile(path string) { f, err := os.Open(path) if err != nil { return } defer f.Close() scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } k, v, ok := strings.Cut(line, "=") if !ok { continue } k = strings.TrimSpace(k) v = strings.TrimSpace(v) if os.Getenv(k) == "" { os.Setenv(k, v) } } } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback }