package config import ( "strconv" "strings" "time" "knowfoolery/backend/services/gateway-service/internal/infra/routing" sharedredis "knowfoolery/backend/shared/infra/database/redis" "knowfoolery/backend/shared/infra/observability/logging" "knowfoolery/backend/shared/infra/observability/metrics" "knowfoolery/backend/shared/infra/observability/tracing" "knowfoolery/backend/shared/infra/utils/envutil" ) // CORSConfig controls gateway CORS behavior. type CORSConfig struct { AllowedOrigins []string AllowedMethods string AllowedHeaders string AllowCredentials bool MaxAgeSeconds int } // SecurityHeadersConfig controls security-related HTTP response headers. type SecurityHeadersConfig struct { ContentSecurityPolicy string EnableHSTS bool HSTSMaxAge int FrameOptions string ContentTypeOptions bool ReferrerPolicy string PermissionsPolicy string } // RateLimitConfig controls request rate limits. type RateLimitConfig struct { GeneralRequests int AuthRequests int APIRequests int AdminRequests int Window time.Duration } // Config is the runtime configuration for gateway-service. type Config struct { AppName string Port int PublicPrefix string UpstreamTimeout time.Duration Upstreams routing.Upstreams CORS CORSConfig Security SecurityHeadersConfig Rate RateLimitConfig Redis sharedredis.Config Tracing tracing.Config Metrics metrics.Config Logging logging.Config ZitadelBaseURL string ZitadelIssuer string ZitadelAudience string ZitadelClientID string ZitadelSecret string } // FromEnv builds config from environment variables. func FromEnv() Config { env := envutil.String("ENVIRONMENT", "development") serviceName := "gateway-service" logCfg := logging.DefaultConfig() logCfg.ServiceName = serviceName logCfg.Environment = env logCfg.Level = envutil.String("LOG_LEVEL", logCfg.Level) traceCfg := tracing.ConfigFromEnv() if traceCfg.ServiceName == "knowfoolery" { traceCfg.ServiceName = serviceName } traceCfg.Environment = env metricsCfg := metrics.ConfigFromEnv() if metricsCfg.ServiceName == "knowfoolery" { metricsCfg.ServiceName = serviceName } prefix := normalizePrefix(envutil.String("GATEWAY_PUBLIC_PREFIX", "/api/v1")) cfg := Config{ AppName: "Know Foolery - Gateway Service", Port: envutil.Int("GATEWAY_INTERNAL_PORT", 18086), PublicPrefix: prefix, UpstreamTimeout: envutil.Duration("GATEWAY_UPSTREAM_TIMEOUT", 3*time.Second), Upstreams: routing.Upstreams{ GameSession: envutil.String("GAME_SESSION_BASE_URL", "http://localhost:8080"), QuestionBank: envutil.String("QUESTION_BANK_BASE_URL", "http://localhost:8081"), User: envutil.String("USER_SERVICE_BASE_URL", "http://localhost:8082"), Leaderboard: envutil.String("LEADERBOARD_BASE_URL", "http://localhost:8083"), Admin: envutil.String("ADMIN_SERVICE_BASE_URL", "http://localhost:8085"), }, CORS: CORSConfig{ AllowedOrigins: parseCSV(envutil.String("GATEWAY_ALLOWED_ORIGINS", "http://localhost:5173")), AllowedMethods: envutil.String("GATEWAY_ALLOWED_METHODS", "GET,POST,PUT,DELETE,OPTIONS"), AllowedHeaders: envutil.String("GATEWAY_ALLOWED_HEADERS", "Origin,Content-Type,Accept,Authorization"), AllowCredentials: parseBool( "GATEWAY_ALLOW_CREDENTIALS", true, ), MaxAgeSeconds: envutil.Int("GATEWAY_CORS_MAX_AGE_SECONDS", 300), }, Security: SecurityHeadersConfig{ ContentSecurityPolicy: envutil.String( "GATEWAY_CSP", "default-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'", ), EnableHSTS: parseBool("GATEWAY_ENABLE_HSTS", true), HSTSMaxAge: envutil.Int("GATEWAY_HSTS_MAX_AGE", 31536000), FrameOptions: envutil.String("GATEWAY_FRAME_OPTIONS", "DENY"), ContentTypeOptions: parseBool("GATEWAY_CONTENT_TYPE_OPTIONS", true), ReferrerPolicy: envutil.String("GATEWAY_REFERRER_POLICY", "strict-origin-when-cross-origin"), PermissionsPolicy: envutil.String( "GATEWAY_PERMISSIONS_POLICY", "geolocation=(), microphone=(), camera=(), payment=(), usb=()", ), }, Rate: RateLimitConfig{ GeneralRequests: envutil.Int("GATEWAY_RATE_GENERAL", 100), AuthRequests: envutil.Int("GATEWAY_RATE_AUTH", 5), APIRequests: envutil.Int("GATEWAY_RATE_API", 60), AdminRequests: envutil.Int("GATEWAY_RATE_ADMIN", 30), Window: envutil.Duration("GATEWAY_RATE_WINDOW", time.Minute), }, Redis: sharedredis.ConfigFromEnv(), Tracing: traceCfg, Metrics: metricsCfg, Logging: logCfg, ZitadelBaseURL: envutil.String("ZITADEL_URL", ""), ZitadelIssuer: envutil.String("ZITADEL_ISSUER", ""), ZitadelAudience: envutil.String("ZITADEL_AUDIENCE", ""), ZitadelClientID: envutil.String("ZITADEL_CLIENT_ID", ""), ZitadelSecret: envutil.String("ZITADEL_CLIENT_SECRET", ""), } return cfg } func normalizePrefix(raw string) string { trimmed := strings.TrimSpace(raw) if trimmed == "" { return "/api/v1" } if !strings.HasPrefix(trimmed, "/") { trimmed = "/" + trimmed } return strings.TrimRight(trimmed, "/") } func parseCSV(raw string) []string { if strings.TrimSpace(raw) == "" { return nil } parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) for _, part := range parts { v := strings.TrimSpace(part) if v == "" { continue } out = append(out, v) } return out } func parseBool(key string, fallback bool) bool { raw := envutil.String(key, "") if raw == "" { return fallback } parsed, err := strconv.ParseBool(raw) if err != nil { return fallback } return parsed }