package liveview import ( "html/template" "net/http" "yovision.local/sense/app/sense/identity" "yovision.local/sense/internal/platform" ) type Module struct{ service *Service } func NewModule(service *Service) *Module { return &Module{service: service} } func (m *Module) Register(app *platform.App) { app.Handle("GET /api/v1/liveview/routes", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.routes))) app.Handle("POST /api/v1/liveview/sessions", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.create))) app.Handle("GET /api/v1/liveview/sessions/{id}", identity.Require(identity.PermissionMediaRead, http.HandlerFunc(m.get))) app.Handle("GET /api/v1/liveview/sessions/{id}/player", identity.RequireSameOriginFrame(identity.PermissionMediaRead, http.HandlerFunc(m.player))) } func (m *Module) routes(w http.ResponseWriter, r *http.Request) { items, err := m.service.Routes(r.Context()) if err != nil { platform.WriteError(w, err) return } platform.WriteJSON(w, http.StatusOK, map[string]any{"items": items}) } func (m *Module) create(w http.ResponseWriter, r *http.Request) { var req struct { RouteID string `json:"route_id"` } if err := platform.DecodeJSON(r, &req); err != nil { platform.WriteError(w, err) return } principal, _ := identity.PrincipalFromContext(r.Context()) session, err := m.service.Create(r.Context(), principal.UserID, req.RouteID) if err != nil { platform.WriteError(w, &platform.APIError{Status: http.StatusBadRequest, Code: "liveview_unavailable", Message: err.Error()}) return } platform.WriteJSON(w, http.StatusCreated, session) } func (m *Module) get(w http.ResponseWriter, r *http.Request) { principal, _ := identity.PrincipalFromContext(r.Context()) session, err := m.service.Get(r.Context(), principal.UserID, r.PathValue("id")) if err != nil { platform.WriteError(w, &platform.APIError{Status: http.StatusGone, Code: "session_expired", Message: "播放会话已过期,请重新打开"}) return } platform.WriteJSON(w, http.StatusOK, session) } var playerTemplate = template.Must(template.New("player").Parse(``)) func (m *Module) player(w http.ResponseWriter, r *http.Request) { principal, _ := identity.PrincipalFromContext(r.Context()) target, err := m.service.EmbedURL(r.Context(), principal.UserID, r.PathValue("id")) if err != nil { http.Error(w, "播放会话已过期", http.StatusGone) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") // This endpoint is the authenticated, same-origin wrapper loaded by the // live-view page. Keep the global DENY policy everywhere else, and allow // only Sense itself to embed this wrapper. w.Header().Set("X-Frame-Options", "SAMEORIGIN") w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'self'; frame-src http: https:; style-src 'unsafe-inline'") _ = playerTemplate.Execute(w, target) }