feat(playground): endpoints rooms/members + CORS para la SPA
Madura el gateway web para servir a una SPA en otro origen: - GET /api/rooms?peer=: rooms que conoce un peer (creadas o unidas). - GET /api/members?room_id=: proxy al control plane (endpoint + rol). - withCORS: middleware con preflight OPTIONS y headers permisivos para el dev server de Vite (mismo modelo de confianza de red que el control plane). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+78
-1
@@ -24,6 +24,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -124,6 +125,22 @@ func (p *peerState) setRoom(roomID string, info roomInfo) {
|
||||
p.mu.Unlock()
|
||||
}
|
||||
|
||||
// roomList returns a snapshot of the rooms this peer knows (created or joined),
|
||||
// so the SPA can render the peer's room list without re-deriving it client-side.
|
||||
func (p *peerState) roomList() []map[string]any {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
out := make([]map[string]any, 0, len(p.rooms))
|
||||
for id, info := range p.rooms {
|
||||
out = append(out, map[string]any{
|
||||
"room_id": id,
|
||||
"subject": info.subject,
|
||||
"encrypt": info.encrypt,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hub: the registry of peers, protected by a single mutex.
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -449,6 +466,64 @@ func (h *Hub) handleKick(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "kicked", "target": req.Target})
|
||||
}
|
||||
|
||||
// handleRooms returns the rooms a peer knows (created or joined). The SPA polls
|
||||
// or calls this after create/join to refresh its room list.
|
||||
//
|
||||
// GET /api/rooms?peer=ana
|
||||
func (h *Hub) handleRooms(w http.ResponseWriter, r *http.Request) {
|
||||
name := r.URL.Query().Get("peer")
|
||||
if name == "" {
|
||||
writeErr(w, http.StatusBadRequest, "peer query param required")
|
||||
return
|
||||
}
|
||||
p, ok := h.lookup(name)
|
||||
if !ok {
|
||||
writeErr(w, http.StatusBadRequest, "unknown peer "+name)
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, p.roomList())
|
||||
}
|
||||
|
||||
// handleMembers lists the members of a room (endpoint id + role) so the SPA can
|
||||
// render a members panel and drive invite/kick. It proxies the control plane's
|
||||
// unauthenticated read endpoint; the public keys it returns are not secret.
|
||||
//
|
||||
// GET /api/members?room_id=<id>
|
||||
func (h *Hub) handleMembers(w http.ResponseWriter, r *http.Request) {
|
||||
roomID := r.URL.Query().Get("room_id")
|
||||
if roomID == "" {
|
||||
writeErr(w, http.StatusBadRequest, "room_id query param required")
|
||||
return
|
||||
}
|
||||
resp, err := http.Get(ctrlURL + "/rooms/" + roomID + "/members")
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusInternalServerError, "fetch members: "+err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(resp.StatusCode)
|
||||
_, _ = w.Write(body)
|
||||
}
|
||||
|
||||
// withCORS allows the SPA running under the Vite dev server (a different origin)
|
||||
// to call the gateway. It answers preflight OPTIONS and tags every response with
|
||||
// permissive CORS headers. v1 trusts the local network, mirroring the control
|
||||
// plane's auth model.
|
||||
func withCORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// handleStream is the SSE endpoint. The browser opens one EventSource per peer;
|
||||
// each received Event is emitted as a `data: <json>\n\n` block. The listener is
|
||||
// cleaned up when the HTTP request context is cancelled (tab closed / reload).
|
||||
@@ -807,9 +882,11 @@ func main() {
|
||||
mux.HandleFunc("POST /api/invite", hub.handleInvite)
|
||||
mux.HandleFunc("POST /api/publish", hub.handlePublish)
|
||||
mux.HandleFunc("POST /api/kick", hub.handleKick)
|
||||
mux.HandleFunc("GET /api/rooms", hub.handleRooms)
|
||||
mux.HandleFunc("GET /api/members", hub.handleMembers)
|
||||
mux.HandleFunc("GET /api/stream", hub.handleStream)
|
||||
mux.HandleFunc("GET /api/bench", hub.handleBench)
|
||||
webSrv := &http.Server{Addr: webAddr, Handler: mux}
|
||||
webSrv := &http.Server{Addr: webAddr, Handler: withCORS(mux)}
|
||||
go func() {
|
||||
if err := webSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("web server: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user