package config import ( "fmt" "os" "strconv" "strings" "time" "smoop-api/internal/vault" ) type MediaMTXConfig struct { APIBase string // e.g. "http://mediamtx:9997" WebRTCBaseURL string // e.g. "http://mediamtx:8889" PublicBaseURL string // e.g. "https://your-host" (for HLS/WHEP URLs returned to SPA) TokenTTL time.Duration // default ~180s } type Config struct { DB struct { DSN string } MinIO struct { Endpoint string AccessKey string SecretKey string UseSSL bool RecordsBucket string LivestreamBucket string PresignTTL time.Duration } MediaMTX MediaMTXConfig JWTSecret []byte PkiIot vault.PKIClient } func Load() (*Config, error) { addr := os.Getenv("VAULT_ADDR") token := os.Getenv("VAULT_TOKEN") // New style: explicit KV v2 mount + key mount := os.Getenv("VAULT_KV_MOUNT") // e.g. "kv" or "secret" key := os.Getenv("VAULT_KV_KEY") // e.g. "snoop" // Back-compat: allow legacy VAULT_KV_PATH like "kv/data/snoop" and derive mount+key if (mount == "" || key == "") && os.Getenv("VAULT_KV_PATH") != "" { legacy := strings.Trim(os.Getenv("VAULT_KV_PATH"), "/") parts := strings.Split(legacy, "/") if len(parts) >= 2 { mount = parts[0] key = parts[len(parts)-1] } } if addr == "" || token == "" || mount == "" || key == "" { return nil, fmt.Errorf("VAULT_ADDR, VAULT_TOKEN, VAULT_KV_MOUNT and VAULT_KV_KEY must be set (or provide legacy VAULT_KV_PATH)") } pki, err := vault.NewPKI(addr, token, "pki_iot", "device", 30*time.Second) if err != nil { return nil, err } raw, err := vault.ReadKVv2(addr, token, mount, key) if err != nil { return nil, err } getStr := func(k string) (string, error) { v, ok := raw[k].(string) if !ok || v == "" { return "", fmt.Errorf("missing secret key: %s", k) } return v, nil } // getStrOpt := func(k, def string) string { // if v, ok := raw[k].(string); ok && v != "" { // return v // } // return def // } getBool := func(k string) (bool, error) { v, ok := raw[k] if !ok { return false, fmt.Errorf("missing secret key: %s", k) } switch t := v.(type) { case bool: return t, nil case string: if t == "true" || t == "1" { return true, nil } return false, nil default: return false, fmt.Errorf("invalid bool for key %s", k) } } getTTL := func(k string, def time.Duration) time.Duration { if v, ok := raw[k].(string); ok && strings.TrimSpace(v) != "" { if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n > 0 { return time.Duration(n) * time.Second } } return def } // --- NEW: MediaMTX config FROM ENV (NOT from Vault) getRequiredEnv := func(k string) (string, error) { v := strings.TrimSpace(os.Getenv(k)) if v == "" { return "", fmt.Errorf("missing required env %s", k) } return v, nil } getIntEnv := func(k string, def int) int { if v := strings.TrimSpace(os.Getenv(k)); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return def } dbDSN, err := getStr("db_dsn") if err != nil { return nil, err } endpoint, err := getStr("minio_endpoint") if err != nil { return nil, err } ak, err := getStr("minio_access_key") if err != nil { return nil, err } sk, err := getStr("minio_secret_key") if err != nil { return nil, err } useSSL, err := getBool("minio_use_ssl") if err != nil { return nil, err } jwt, err := getStr("jwt_secret") if err != nil { return nil, err } recordsBucket := "records" if v, ok := raw["minio_records_bucket"].(string); ok && v != "" { recordsBucket = v } liveBucket := "livestream" if v, ok := raw["minio_livestream_bucket"].(string); ok && v != "" { liveBucket = v } // presignTTL := 15 * time.Minute // if v, ok := raw["minio_presign_ttl_seconds"].(string); ok && v != "" { // var sec int // fmt.Sscanf(v, "%d", &sec) // if sec > 0 { // presignTTL = time.Duration(sec) * time.Second // } // } presignTTL := getTTL("minio_presign_ttl_seconds", 15*time.Minute) apiBase, err := getRequiredEnv("MEDIAMTX_API_BASE") if err != nil { return nil, err } webrtcBase, err := getRequiredEnv("MEDIAMTX_WEBRTC_BASE_URL") if err != nil { return nil, err } publicBase, err := getRequiredEnv("PUBLIC_BASE_URL") if err != nil { return nil, err } tokenTTL := getIntEnv("MEDIAMTX_TOKEN_TTL_SECONDS", 180) cfg := &Config{} cfg.DB.DSN = dbDSN cfg.MinIO.Endpoint = endpoint cfg.MinIO.AccessKey = ak cfg.MinIO.SecretKey = sk cfg.MinIO.UseSSL = useSSL cfg.MinIO.RecordsBucket = recordsBucket cfg.MinIO.LivestreamBucket = liveBucket cfg.MinIO.PresignTTL = presignTTL cfg.JWTSecret = []byte(jwt) cfg.MediaMTX = MediaMTXConfig{ APIBase: apiBase, WebRTCBaseURL: webrtcBase, PublicBaseURL: publicBase, TokenTTL: time.Duration(tokenTTL), } cfg.PkiIot = *pki return cfg, nil } func LoadDev() (*Config, error) { getRequired := func(k string) (string, error) { v := os.Getenv(k) if v == "" { return "", fmt.Errorf("missing required env %s", k) } return v, nil } addr := os.Getenv("VAULT_ADDR") token := os.Getenv("VAULT_TOKEN") pki, err := vault.NewPKI(addr, token, "pki_iot", "device", 30*time.Second) if err != nil { return nil, err } getBoolEnv := func(k string, def bool) bool { v := strings.ToLower(strings.TrimSpace(os.Getenv(k))) if v == "true" || v == "1" || v == "yes" { return true } if v == "false" || v == "0" || v == "no" { return false } return def } getIntEnv := func(k string, def int) int { if v := strings.TrimSpace(os.Getenv(k)); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return def } dbDSN, err := getRequired("DB_DSN") if err != nil { return nil, err } endpoint, err := getRequired("MINIO_ENDPOINT") if err != nil { return nil, err } ak, err := getRequired("MINIO_ACCESS_KEY") if err != nil { return nil, err } sk, err := getRequired("MINIO_SECRET_KEY") if err != nil { return nil, err } jwt, err := getRequired("JWT_SECRET") if err != nil { return nil, err } useSSL := getBoolEnv("MINIO_USE_SSL", false) recordsBucket := os.Getenv("MINIO_RECORDS_BUCKET") if recordsBucket == "" { recordsBucket = "records" } liveBucket := os.Getenv("MINIO_LIVESTREAM_BUCKET") if liveBucket == "" { liveBucket = "livestream" } presignTTL := time.Duration(getIntEnv("MINIO_PRESIGN_TTL_SECONDS", 900)) * time.Second // NEW: MediaMTX envs apiBase, err := getRequired("MEDIAMTX_API_BASE") if err != nil { return nil, err } webrtcBase, err := getRequired("MEDIAMTX_WEBRTC_BASE_URL") if err != nil { return nil, err } publicBase, err := getRequired("PUBLIC_BASE_URL") if err != nil { return nil, err } tokenTTL := getIntEnv("MEDIAMTX_TOKEN_TTL_SECONDS", 180) cfg := &Config{} cfg.DB.DSN = dbDSN cfg.MinIO.Endpoint = endpoint cfg.MinIO.AccessKey = ak cfg.MinIO.SecretKey = sk cfg.MinIO.UseSSL = useSSL cfg.MinIO.RecordsBucket = recordsBucket cfg.MinIO.LivestreamBucket = liveBucket cfg.MinIO.PresignTTL = presignTTL cfg.JWTSecret = []byte(jwt) cfg.MediaMTX = MediaMTXConfig{ APIBase: apiBase, WebRTCBaseURL: webrtcBase, PublicBaseURL: publicBase, TokenTTL: time.Duration(tokenTTL), } cfg.PkiIot = *pki return cfg, nil }