You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

196 lines
5.5 KiB
Go

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
}