Files
Edu/services/push-gateway/internal/config/config.go
SpecialX 6af1aa0d82 feat(push-gateway,msg): redis pubsub backplane for real-time notifications
M7: ADR-040 Redis Pub/Sub as state routing backplane

- push-gateway: remove Kafka consumer, add SSE endpoint

- SSE: subscribe to Redis user:{userId}:notify on connect

- msg: publish notifications to Redis Pub/Sub instead of HTTP push

- docker-compose: remove Kafka env from push-gateway
2026-07-15 01:28:55 +08:00

111 lines
3.7 KiB
Go

// Package config loads runtime configuration for push-gateway from environment
// variables. Required variables (JWT_SECRET in production, PUSH_INTERNAL_TOKEN
// or INTERNAL_API_TOKEN when not in DevMode) cause a fatal exit when missing.
//
// M7 (ADR-040): Kafka config removed — push-gateway no longer mounts Kafka
// directly. Real-time push is routed through Redis Pub/Sub backplane
// (channel: user:{userId}:notify). PUSH_INTERNAL_TOKEN is the canonical env
// var for /internal/* auth (INTERNAL_API_TOKEN kept as a backward-compat
// alias).
package config
import (
"log"
"os"
"strings"
"github.com/edu-cloud/shared-go/env"
)
// Config holds all push-gateway runtime configuration.
type Config struct {
Port string
JWTSecret string
DevMode bool
RedisURL string
OTLPEndpoint string
InternalAPIToken string
// WebSocket
AllowedOrigins []string
MaxConnsPerUser int
HeartbeatInterval int // seconds
// JWT RS256 (via shared-go/jwks, iam /.well-known/jwks.json)
JWKSURL string
// Instance identity (for Redis SET membership)
InstanceID string
}
// devJWTSecret is the default JWT secret used only in DevMode for local
// development. Production deployments MUST set JWT_SECRET.
const devJWTSecret = "p1-dev-secret-change-in-production"
// Load reads configuration from environment variables.
//
// In DevMode (DEV_MODE=true) JWT_SECRET defaults to a dev value and internal
// API token validation is skipped; in production an unset JWT_SECRET or
// INTERNAL_API_TOKEN is fatal.
func Load() *Config {
devMode := env.GetBool("DEV_MODE", false)
jwtSecret := env.Get("JWT_SECRET", "")
if jwtSecret == "" {
if devMode {
log.Println("warning: JWT_SECRET not set, using dev default (DEV_MODE=true)")
jwtSecret = devJWTSecret
} else {
log.Fatal("JWT_SECRET must be set in non-dev mode")
}
}
// PUSH_INTERNAL_TOKEN is the canonical env var for /internal/* auth
// (ARB-013 alignment with msg). INTERNAL_API_TOKEN is kept as a
// backward-compat alias for existing deployments.
internalToken := env.Get("PUSH_INTERNAL_TOKEN", "")
if internalToken == "" {
internalToken = env.Get("INTERNAL_API_TOKEN", "")
}
if internalToken == "" && !devMode {
log.Fatal("PUSH_INTERNAL_TOKEN (or INTERNAL_API_TOKEN) must be set in non-dev mode")
}
return &Config{
Port: env.Get("PUSH_GATEWAY_PORT", "8081"),
JWTSecret: jwtSecret,
DevMode: devMode,
RedisURL: env.Get("REDIS_URL", "redis://localhost:6379/0"),
OTLPEndpoint: env.Get("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:4318"),
InternalAPIToken: internalToken,
AllowedOrigins: parseOrigins(env.Get("WS_ALLOWED_ORIGINS", "")),
MaxConnsPerUser: env.GetInt("MAX_CONNS_PER_USER", 5),
HeartbeatInterval: env.GetInt("HEARTBEAT_INTERVAL_SECONDS", 30),
JWKSURL: env.Get("JWKS_URL", "http://localhost:50052/.well-known/jwks.json"),
InstanceID: env.Get("INSTANCE_ID", generateInstanceID()),
}
}
// parseOrigins splits a comma-separated list of allowed WebSocket origins.
// Empty string yields nil (all origins rejected in production).
func parseOrigins(raw string) []string {
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
if trimmed := strings.TrimSpace(p); trimmed != "" {
out = append(out, trimmed)
}
}
return out
}
// generateInstanceID returns a stable process-unique identifier. Falls back to
// the hostname when INSTANCE_ID is not explicitly set, allowing each replica to
// be uniquely identifiable in the Redis online-presence SET.
func generateInstanceID() string {
host, err := os.Hostname()
if err != nil || host == "" {
return "push-gateway-unknown"
}
return host
}