fix(display): screen UUID lookup, authScreen middleware, JSON encoding

- playerstatus: look up screen by slug before UpsertDisplayState to pass UUID (not slug) and avoid FK violation
- router: switch display command route from authOnly to authScreen for proper permission enforcement
- display.go: remove redundant GetBySlug + requireScreenAccess (now handled by authScreen middleware), drop store dependency
- notifier: replace fmt.Sprintf %q with json.Marshal for correct JSON encoding of display command payload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jesko Anschütz 2026-03-26 23:35:05 +01:00
parent 96135266f1
commit 79fcc20b79
4 changed files with 14 additions and 15 deletions

View file

@ -6,22 +6,13 @@ import (
"net/http"
"git.az-it.net/az/morz-infoboard/server/backend/internal/mqttnotifier"
"git.az-it.net/az/morz-infoboard/server/backend/internal/store"
)
// HandleDisplayCommand nimmt {"state":"on"} oder {"state":"off"} entgegen und
// schickt den entsprechenden MQTT-Befehl an den Agent.
func HandleDisplayCommand(screens *store.ScreenStore, notifier *mqttnotifier.Notifier) http.HandlerFunc {
func HandleDisplayCommand(notifier *mqttnotifier.Notifier) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
screenSlug := r.PathValue("screenSlug")
screen, err := screens.GetBySlug(r.Context(), screenSlug)
if err != nil {
http.Error(w, "screen not found", http.StatusNotFound)
return
}
if !requireScreenAccess(w, r, screen) {
return
}
var body struct {
State string `json:"state"`

View file

@ -135,8 +135,10 @@ func handlePlayerStatus(store playerStatusStore, screenStore *storePackage.Scree
})
if request.DisplayState != "" && screenStore != nil {
if err := screenStore.UpsertDisplayState(r.Context(), request.ScreenID, request.DisplayState); err != nil {
slog.Error("upsert display state", "screen_id", request.ScreenID, "err", err)
if screen, err := screenStore.GetBySlug(r.Context(), request.ScreenID); err == nil {
if err := screenStore.UpsertDisplayState(r.Context(), screen.ID, request.DisplayState); err != nil {
slog.Error("upsert display state", "screen_id", screen.ID, "err", err)
}
}
}

View file

@ -185,7 +185,7 @@ func registerManageRoutes(mux *http.ServeMux, d RouterDeps) {
// ── Display control ───────────────────────────────────────────────────
mux.Handle("POST /api/v1/screens/{screenSlug}/display",
authOnly(http.HandlerFunc(manage.HandleDisplayCommand(d.ScreenStore, notifier))))
authScreen(http.HandlerFunc(manage.HandleDisplayCommand(notifier))))
// ── JSON API — screens ────────────────────────────────────────────────
// Self-registration: no auth (player calls this on startup).

View file

@ -4,6 +4,7 @@
package mqttnotifier
import (
"encoding/json"
"fmt"
"sync"
"time"
@ -103,8 +104,13 @@ func (n *Notifier) SendDisplayCommand(screenSlug, action string) error {
return nil
}
topic := fmt.Sprintf("signage/screen/%s/command", screenSlug)
payload := []byte(fmt.Sprintf(`{"action":%q}`, action))
token := n.client.Publish(topic, 1, true, payload)
b, err := json.Marshal(struct {
Action string `json:"action"`
}{Action: action})
if err != nil {
return fmt.Errorf("marshal display command: %w", err)
}
token := n.client.Publish(topic, 1, true, b)
if !token.WaitTimeout(5 * time.Second) {
return fmt.Errorf("mqtt publish display command: timeout")
}