package api import ( "encoding/json" "net/http" "time" "github.com/alchemistkay/guestguard/internal/domain" "github.com/alchemistkay/guestguard/internal/storage" ) type wsTicketHandler struct { tickets *wsTicketStore events *storage.EventRepo collabs *storage.CollaboratorRepo } type wsTicketResponse struct { Ticket string `json:"ticket"` ExpiresAt time.Time `json:"expires_at"` } // POST /auth/ws-ticket — requireAuth-protected; body { "event_id": "" }. // Returns a single-use ticket valid for ~60 seconds. The frontend appends it // as `?ticket=…` on the WebSocket URL. func (h *wsTicketHandler) issue(w http.ResponseWriter, r *http.Request) { hostID, ok := hostFromContext(w, r) if !ok { return } var req struct { EventID string `json:"event_id"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeError(w, http.StatusBadRequest, "invalid json") return } eventID, ok := parseRawUUID(w, "event_id", req.EventID) if !ok { return } // Block C: viewers can subscribe to the live monitor too — the WS stream // only carries read-only events (RSVPs, fraud scores, check-ins). if _, _, ok := requireRole(w, r, h.events, h.collabs, eventID, hostID, domain.RoleViewer); !ok { return } tok, exp, err := h.tickets.Mint(hostID, eventID) if err != nil { writeError(w, http.StatusInternalServerError, "failed to mint ticket") return } writeJSON(w, http.StatusOK, wsTicketResponse{Ticket: tok, ExpiresAt: exp}) }