package config import ( "fmt" "os" "slices" "strconv" "strings" "time" "github.com/joho/godotenv" ) type Config struct { Env string AppName string HTTP HTTPConfig CORS CORSConfig Postgres PostgresConfig Cache CacheConfig Auth AuthConfig TMDB TMDBConfig IGDB IGDBConfig } type HTTPConfig struct { Host string Port int ReadTimeout time.Duration WriteTimeout time.Duration } type CORSConfig struct { AllowedOrigins []string AllowedMethods []string AllowedHeaders []string ExposedHeaders []string AllowCredentials bool MaxAge time.Duration } type PostgresConfig struct { URL string MaxConns int32 MinConns int32 } type CacheConfig struct { Addr string Password string DB int } type AuthConfig struct { AccessTokenTTLMinutes int RefreshTokenTTLHours int JWTSecret string } type TMDBConfig struct { APIKey string BaseURL string } type IGDBConfig struct { ClientID string ClientSecret string BaseURL string TokenURL string } func Load() Config { _ = godotenv.Load() return Config{ Env: getString("SEEN_ENV", "development"), AppName: getString("SEEN_APP_NAME", "seen"), HTTP: HTTPConfig{ Host: getString("SEEN_HTTP_HOST", "0.0.0.0"), Port: getInt("SEEN_HTTP_PORT", 8081), ReadTimeout: getDuration("SEEN_HTTP_READ_TIMEOUT", 15*time.Second), WriteTimeout: getDuration("SEEN_HTTP_WRITE_TIMEOUT", 15*time.Second), }, CORS: CORSConfig{ AllowedOrigins: getCSV("SEEN_CORS_ALLOWED_ORIGINS", []string{}), AllowedMethods: getCSV("SEEN_CORS_ALLOWED_METHODS", []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}), AllowedHeaders: getCSV("SEEN_CORS_ALLOWED_HEADERS", []string{"Accept", "Authorization", "Content-Type", "X-Request-ID"}), ExposedHeaders: getCSV("SEEN_CORS_EXPOSED_HEADERS", []string{"X-Request-ID"}), AllowCredentials: getBool("SEEN_CORS_ALLOW_CREDENTIALS", false), MaxAge: getDuration("SEEN_CORS_MAX_AGE", 24*time.Hour), }, Postgres: PostgresConfig{ URL: getString("SEEN_POSTGRES_URL", "postgres://seen:seen@localhost:5432/seen?sslmode=disable"), MaxConns: int32(getInt("SEEN_POSTGRES_MAX_CONNS", 10)), MinConns: int32(getInt("SEEN_POSTGRES_MIN_CONNS", 2)), }, Cache: CacheConfig{ Addr: getString("SEEN_CACHE_ADDR", "localhost:6379"), Password: getString("SEEN_CACHE_PASSWORD", ""), DB: getInt("SEEN_CACHE_DB", 0), }, Auth: AuthConfig{ AccessTokenTTLMinutes: getInt("SEEN_AUTH_ACCESS_TOKEN_TTL_MINUTES", 30), RefreshTokenTTLHours: getInt("SEEN_AUTH_REFRESH_TOKEN_TTL_HOURS", 24*30), JWTSecret: getString("SEEN_AUTH_JWT_SECRET", "replace-in-production"), }, TMDB: TMDBConfig{ APIKey: getString("SEEN_TMDB_API_KEY", ""), BaseURL: getString("SEEN_TMDB_BASE_URL", "https://api.themoviedb.org/3"), }, IGDB: IGDBConfig{ ClientID: getString("SEEN_IGDB_CLIENT_ID", ""), ClientSecret: getString("SEEN_IGDB_CLIENT_SECRET", ""), BaseURL: getString("SEEN_IGDB_BASE_URL", "https://api.igdb.com/v4"), TokenURL: getString("SEEN_IGDB_TOKEN_URL", "https://id.twitch.tv/oauth2/token"), }, } } func (c HTTPConfig) Addr() string { return fmt.Sprintf("%s:%d", c.Host, c.Port) } func getString(key string, fallback string) string { if value, ok := os.LookupEnv(key); ok { return value } return fallback } func getInt(key string, fallback int) int { value, ok := os.LookupEnv(key) if !ok { return fallback } parsed, err := strconv.Atoi(value) if err != nil { return fallback } return parsed } func getDuration(key string, fallback time.Duration) time.Duration { value, ok := os.LookupEnv(key) if !ok { return fallback } parsed, err := time.ParseDuration(value) if err != nil { return fallback } return parsed } func getBool(key string, fallback bool) bool { value, ok := os.LookupEnv(key) if !ok { return fallback } switch strings.ToLower(strings.TrimSpace(value)) { case "1", "true", "yes", "on": return true case "0", "false", "no", "off": return false default: return fallback } } func getCSV(key string, fallback []string) []string { value, ok := os.LookupEnv(key) if !ok { return slices.Clone(fallback) } parts := strings.Split(value, ",") items := make([]string, 0, len(parts)) for _, part := range parts { trimmed := strings.TrimSpace(part) if trimmed == "" { continue } items = append(items, trimmed) } if len(items) == 0 { return slices.Clone(fallback) } return items }