package handlers import ( "net/http" "veza-backend-api/internal/config" "github.com/gin-gonic/gin" ) // IceServer mirrors the WebRTC `RTCIceServer` dictionary. Username and // credential are omitted (not zero-valued) when empty so STUN-only // entries don't carry meaningless auth fields, and so frontends that // validate the response don't have to special-case empty strings. // // Wire shape is intentionally lowercase to match the JS API the // frontend feeds directly into `new RTCPeerConnection({ iceServers })`. type IceServer struct { URLs []string `json:"urls"` Username string `json:"username,omitempty"` Credential string `json:"credential,omitempty"` } // WebRTCConfigResponse is the body of GET /api/v1/config/webrtc. The // frontend caches it for the lifetime of the page and uses it to build // every RTCPeerConnection (1:1 calls today, screen-share / multi-party // later). Public endpoint by design — TURN credentials returned here // are short-lived rotation candidates, never long-lived secrets. type WebRTCConfigResponse struct { IceServers []IceServer `json:"iceServers"` } // GetWebRTCConfig returns the ICE-server set the frontend should hand to // `new RTCPeerConnection`. v1.0.9 item 1.2 — closes the // FUNCTIONAL_AUDIT.md §4 #1 NAT-traversal gap. Before this endpoint the // frontend hardcoded `stun:stun.l.google.com:19302`, which works on // flat networks but fails behind symmetric NAT (corporate, mobile // carrier, Incus container default networking). Returning the operator- // configured TURN block lets the browser fall back to a relay when // hole-punching fails, which is the only thing that makes WebRTC reach // "actually works for end users" status. // // @Summary WebRTC ICE configuration // @Description Public — returns the ICE-server set the SPA feeds to RTCPeerConnection. STUN-only when no TURN is configured. TURN credentials are always emitted as static (REST shared-secret rotation deferred to v1.1). // @Tags Config // @Produce json // @Success 200 {object} handlers.WebRTCConfigResponse "ICE servers" // @Router /config/webrtc [get] func GetWebRTCConfig(cfg *config.Config) gin.HandlerFunc { return func(c *gin.Context) { resp := WebRTCConfigResponse{ IceServers: []IceServer{}, } if cfg != nil && len(cfg.WebRTCStunURLs) > 0 { resp.IceServers = append(resp.IceServers, IceServer{URLs: cfg.WebRTCStunURLs}) } // TURN block is only emitted when fully configured. Half-configured // is worse than missing — the browser would surface auth failures // instead of falling back cleanly to STUN-only. if cfg != nil && len(cfg.WebRTCTurnURLs) > 0 && cfg.WebRTCTurnUsername != "" && cfg.WebRTCTurnCredential != "" { resp.IceServers = append(resp.IceServers, IceServer{ URLs: cfg.WebRTCTurnURLs, Username: cfg.WebRTCTurnUsername, Credential: cfg.WebRTCTurnCredential, }) } c.JSON(http.StatusOK, resp) } }