130 lines
4.4 KiB
Go
130 lines
4.4 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.
|
|
//
|
|
// v2 alignment (ARB-013 / msg contract): Kafka topic default changed to
|
|
// edu.notify.notification.sent; 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
|
|
// Kafka
|
|
KafkaBrokers []string
|
|
KafkaNotificationTopic string
|
|
KafkaConsumerGroup 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"),
|
|
KafkaBrokers: parseBrokers(env.Get("KAFKA_BROKERS", "localhost:9092")),
|
|
// ARB-013 canonical topic name: edu.<domain>.<aggregate>.<action>.
|
|
// msg's Outbox publisher emits to this topic.
|
|
KafkaNotificationTopic: env.Get("KAFKA_NOTIFICATION_TOPIC", "edu.notify.notification.sent"),
|
|
KafkaConsumerGroup: env.Get("KAFKA_CONSUMER_GROUP", "push-gateway"),
|
|
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
|
|
}
|
|
|
|
// parseBrokers splits a comma-separated list of Kafka broker addresses.
|
|
func parseBrokers(raw string) []string {
|
|
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
|
|
}
|