fix(tts): move TTS endpoints from /mgmt to /setup (no Basic Auth)

The TTS speak/config endpoints were under /mgmt (Basic-Auth protected),
but the soundtouch-web proxy and CLI authenticated with their own
mgmt-password default (empty) while the service defaults to "change_me!",
so speaking from -web returned 401.

This was also inconsistent: the Google API key is configured via the
unauthenticated /setup/settings, and Play URL already proxies to /setup,
so gating only TTS playback behind mgmt auth made no sense. Move
/mgmt/tts/{speak,config} to /setup/tts/{speak,config} (LAN-trust, like
the rest of the setup surface), rename the handlers accordingly, and drop
the now-unused mgmt-credential plumbing from soundtouch-web and the CLI
tts command.

Verified: POST /setup/tts/speak now reaches the handler without auth
(502 only because the test speaker IP is unreachable; previously 401).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-05-31 22:35:31 +02:00
co-authored by Claude Opus 4.8
parent 8f2939a9a6
commit 169c1c5b9f
10 changed files with 31 additions and 64 deletions
+4 -4
View File
@@ -25,9 +25,9 @@ type ttsSpeakRequest struct {
Volume *int `json:"volume,omitempty"`
}
// HandleMgmtTTSSpeak synthesizes the requested text (or builds a direct URL),
// HandleTTSSpeak synthesizes the requested text (or builds a direct URL),
// then tells the target speaker to play it via the /speaker endpoint.
func (s *Server) HandleMgmtTTSSpeak(w http.ResponseWriter, r *http.Request) {
func (s *Server) HandleTTSSpeak(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
svc := s.ttsSvc()
@@ -147,8 +147,8 @@ func (s *Server) HandleTTSMedia(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(audio)
}
// HandleMgmtTTSConfig reports the active TTS configuration (no secrets).
func (s *Server) HandleMgmtTTSConfig(w http.ResponseWriter, _ *http.Request) {
// HandleTTSConfig reports the active TTS configuration (no secrets).
func (s *Server) HandleTTSConfig(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/json")
svc := s.ttsSvc()
+10 -10
View File
@@ -23,8 +23,8 @@ func ttsTestRouter(t *testing.T, baseURL string) (*chi.Mux, *Server) {
r := chi.NewRouter()
r.Get("/media/tts/{id}", server.HandleTTSMedia)
r.Post("/mgmt/tts/speak", server.HandleMgmtTTSSpeak)
r.Get("/mgmt/tts/config", server.HandleMgmtTTSConfig)
r.Post("/setup/tts/speak", server.HandleTTSSpeak)
r.Get("/setup/tts/config", server.HandleTTSConfig)
return r, server
}
@@ -40,10 +40,10 @@ func mockCloudTTS(t *testing.T, audio string) *httptest.Server {
}))
}
func TestHandleMgmtTTSConfigNotConfigured(t *testing.T) {
func TestHandleTTSConfigNotConfigured(t *testing.T) {
r, _ := ttsTestRouter(t, "http://localhost:8001")
req := httptest.NewRequest(http.MethodGet, "/mgmt/tts/config", nil)
req := httptest.NewRequest(http.MethodGet, "/setup/tts/config", nil)
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
@@ -61,11 +61,11 @@ func TestHandleMgmtTTSConfigNotConfigured(t *testing.T) {
}
}
func TestHandleMgmtTTSConfigConfigured(t *testing.T) {
func TestHandleTTSConfigConfigured(t *testing.T) {
r, server := ttsTestRouter(t, "http://localhost:8001")
server.SetTTSService(tts.NewService(tts.NewTranslateProvider(), tts.Config{AppKey: "k", DefaultLanguage: "EN"}))
req := httptest.NewRequest(http.MethodGet, "/mgmt/tts/config", nil)
req := httptest.NewRequest(http.MethodGet, "/setup/tts/config", nil)
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
@@ -139,10 +139,10 @@ func TestHandleTTSMediaMissingClip(t *testing.T) {
}
}
func TestHandleMgmtTTSSpeakNotConfigured(t *testing.T) {
func TestHandleTTSSpeakNotConfigured(t *testing.T) {
r, _ := ttsTestRouter(t, "http://localhost:8001")
req := httptest.NewRequest(http.MethodPost, "/mgmt/tts/speak", strings.NewReader(`{"host":"192.0.2.10","text":"hi"}`))
req := httptest.NewRequest(http.MethodPost, "/setup/tts/speak", strings.NewReader(`{"host":"192.0.2.10","text":"hi"}`))
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
@@ -151,7 +151,7 @@ func TestHandleMgmtTTSSpeakNotConfigured(t *testing.T) {
}
}
func TestHandleMgmtTTSSpeakValidation(t *testing.T) {
func TestHandleTTSSpeakValidation(t *testing.T) {
r, server := ttsTestRouter(t, "http://localhost:8001")
server.SetTTSService(tts.NewService(tts.NewTranslateProvider(), tts.Config{AppKey: "k"}))
@@ -167,7 +167,7 @@ func TestHandleMgmtTTSSpeakValidation(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/mgmt/tts/speak", strings.NewReader(tc.body))
req := httptest.NewRequest(http.MethodPost, "/setup/tts/speak", strings.NewReader(tc.body))
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
-5
View File
@@ -42,11 +42,6 @@ type WebApp struct {
RepoURL string
ServiceURL string
// Management API credentials for proxying to the AfterTouch service's
// Basic-Auth-protected /mgmt endpoints (e.g. TTS synthesis).
MgmtUsername string
MgmtPassword string
discoveryStatus atomic.Value // stores *webtypes.DiscoveryStatus
}
+2 -6
View File
@@ -14,7 +14,7 @@ import (
// HandleAPISpeakText synthesizes and plays text on a device. The Web UI talks
// to speakers directly for most controls, but TTS synthesis (Google Cloud) and
// the Bose app_key live in the AfterTouch service, so this proxies to the
// service's /mgmt/tts/speak endpoint, targeting the device by its IP/host.
// service's /setup/tts/speak endpoint, targeting the device by its IP/host.
func (app *WebApp) HandleAPISpeakText(w http.ResponseWriter, r *http.Request) {
deviceID := chi.URLParam(r, "id")
@@ -83,7 +83,7 @@ func (app *WebApp) HandleAPISpeakText(w http.ResponseWriter, r *http.Request) {
return
}
upstream, err := http.NewRequestWithContext(r.Context(), http.MethodPost, serviceURL+"/mgmt/tts/speak", bytes.NewReader(body))
upstream, err := http.NewRequestWithContext(r.Context(), http.MethodPost, serviceURL+"/setup/tts/speak", bytes.NewReader(body))
if err != nil {
app.sendError(w, "Failed to build TTS request", http.StatusInternalServerError)
return
@@ -91,10 +91,6 @@ func (app *WebApp) HandleAPISpeakText(w http.ResponseWriter, r *http.Request) {
upstream.Header.Set("Content-Type", "application/json")
if app.MgmtUsername != "" || app.MgmtPassword != "" {
upstream.SetBasicAuth(app.MgmtUsername, app.MgmtPassword)
}
resp, err := http.DefaultClient.Do(upstream)
if err != nil {
app.sendError(w, fmt.Sprintf("TTS service request failed: %v", err), http.StatusBadGateway)
+1 -1
View File
@@ -79,7 +79,7 @@ func (app *WebApp) Mount(r chi.Router, discoveryService *discovery.UnifiedDiscov
// Custom URL playback
r.Post("/api/play-url/{id}", app.HandlePlayURL)
// Text-to-speech (proxied to the AfterTouch service's /mgmt/tts/speak)
// Text-to-speech (proxied to the AfterTouch service's /setup/tts/speak)
r.Post("/api/device-speak/{id}", app.HandleAPISpeakText)
// SPA routes — serve index.html for client-side routing