From 169c1c5b9f4b66201725795c7bbb397aedcc6083 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 31 May 2026 20:39:10 +0200 Subject: [PATCH] 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) --- cmd/soundtouch-cli/cmd_tts.go | 14 +------------ cmd/soundtouch-service/main.go | 10 +++++----- .../testdata/router_routes.txt | 4 ++-- cmd/soundtouch-web/main.go | 13 ------------ .../docs/reference/SPEAKER-ENDPOINT.md | 11 +++++----- pkg/service/handlers/handlers_tts.go | 8 ++++---- pkg/service/handlers/handlers_tts_test.go | 20 +++++++++---------- pkg/service/soundtouchweb/handler.go | 5 ----- pkg/service/soundtouchweb/handlers_tts.go | 8 ++------ pkg/service/soundtouchweb/mount.go | 2 +- 10 files changed, 31 insertions(+), 64 deletions(-) diff --git a/cmd/soundtouch-cli/cmd_tts.go b/cmd/soundtouch-cli/cmd_tts.go index 00262b4..fe532af 100644 --- a/cmd/soundtouch-cli/cmd_tts.go +++ b/cmd/soundtouch-cli/cmd_tts.go @@ -65,17 +65,6 @@ func ttsSpeakCmd() *cli.Command { Aliases: []string{"v"}, Usage: "Playback volume (0-100, 0 = service default)", }, - &cli.StringFlag{ - Name: "mgmt-username", - Usage: "Management API username for HTTP Basic Auth", - Value: "admin", - EnvVars: []string{"MGMT_USERNAME"}, - }, - &cli.StringFlag{ - Name: "mgmt-password", - Usage: "Management API password for HTTP Basic Auth", - EnvVars: []string{"MGMT_PASSWORD"}, - }, ), Action: ttsSpeak, } @@ -116,13 +105,12 @@ func ttsSpeak(c *cli.Context) error { return fmt.Errorf("marshal request: %w", err) } - req, err := http.NewRequest(http.MethodPost, serviceURL+"/mgmt/tts/speak", bytes.NewReader(body)) + req, err := http.NewRequest(http.MethodPost, serviceURL+"/setup/tts/speak", bytes.NewReader(body)) if err != nil { return fmt.Errorf("build request: %w", err) } req.Header.Set("Content-Type", "application/json") - req.SetBasicAuth(c.String("mgmt-username"), c.String("mgmt-password")) resp, err := http.DefaultClient.Do(req) if err != nil { diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index dfc3471..d657b88 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -1321,11 +1321,6 @@ func setupRouter(server *handlers.Server, stockholmHandler *stockholm.Handler) * r.Post("/prime", server.HandleMgmtPrimeDeviceAmazon) }) - r.Route("/tts", func(r chi.Router) { - r.Post("/speak", server.HandleMgmtTTSSpeak) - r.Get("/config", server.HandleMgmtTTSConfig) - }) - r.Get("/devices/{deviceId}/events", server.HandleMgmtDeviceEvents) }) }) @@ -1338,6 +1333,11 @@ func setupRouter(server *handlers.Server, stockholmHandler *stockholm.Handler) * r.Get("/discovery-status", server.HandleGetDiscoveryStatus) r.Get("/settings", server.HandleGetSettings) r.Post("/settings", server.HandleUpdateSettings) + // TTS lives under /setup (LAN-trust, like the rest of the integration + // surface and Play URL), not /mgmt: the API key is already configured + // via /setup/settings, and -web/CLI reach this without mgmt credentials. + r.Post("/tts/speak", server.HandleTTSSpeak) + r.Get("/tts/config", server.HandleTTSConfig) r.Get("/info/{deviceId}", server.HandleGetDeviceInfo) r.Get("/summary/{deviceId}", server.HandleGetMigrationSummary) r.Post("/migrate/{deviceId}", server.HandleMigrateDevice) diff --git a/cmd/soundtouch-service/testdata/router_routes.txt b/cmd/soundtouch-service/testdata/router_routes.txt index 350093f..413d75b 100644 --- a/cmd/soundtouch-service/testdata/router_routes.txt +++ b/cmd/soundtouch-service/testdata/router_routes.txt @@ -59,7 +59,6 @@ GET /mgmt/devices/{deviceId}/events handlers.( GET /mgmt/spotify/accounts handlers.(*Server).HandleMgmtSpotifyAccounts-fm GET /mgmt/spotify/callback handlers.(*Server).HandleMgmtSpotifyCallback-fm GET /mgmt/spotify/token handlers.(*Server).HandleMgmtSpotifyToken-fm -GET /mgmt/tts/config handlers.(*Server).HandleMgmtTTSConfig-fm GET /setup/account-id-suggestions/{deviceId} handlers.(*Server).HandleAccountIDSuggestions-fm GET /setup/ca.crt handlers.(*Server).HandleGetCACert-fm GET /setup/device-summary/{deviceId} handlers.(*Server).HandleDeviceSummary-fm @@ -79,6 +78,7 @@ GET /setup/logging-settings handlers.( GET /setup/logs handlers.(*Server).HandleGetLogs-fm GET /setup/settings handlers.(*Server).HandleGetSettings-fm GET /setup/summary/{deviceId} handlers.(*Server).HandleGetMigrationSummary-fm +GET /setup/tts/config handlers.(*Server).HandleTTSConfig-fm GET /setup/version handlers.(*Server).HandleGetVersionInfo-fm GET /streaming/account/{account}/device/{device}/group handlers.(*Server).HandleMargeDeviceGroup-fm GET /streaming/account/{account}/device/{device}/group/ handlers.(*Server).HandleMargeDeviceGroup-fm @@ -132,7 +132,6 @@ POST /mgmt/spotify/confirm handlers.( POST /mgmt/spotify/entity handlers.(*Server).HandleMgmtSpotifyEntity-fm POST /mgmt/spotify/init handlers.(*Server).HandleMgmtSpotifyInit-fm POST /mgmt/spotify/prime handlers.(*Server).HandleMgmtPrimeDevice-fm -POST /mgmt/tts/speak handlers.(*Server).HandleMgmtTTSSpeak-fm POST /oauth/account/{account}/music/musicprovider/{sourceID}/token/cs handlers.(*Server).HandleBoseAccountToken-fm POST /oauth/device/{deviceID}/music/musicprovider/{sourceID}/token handlers.(*Server).HandleBoseLegacyToken-fm POST /oauth/device/{deviceID}/music/musicprovider/{sourceID}/token/cs1 handlers.(*Server).HandleBoseToken-fm @@ -155,6 +154,7 @@ POST /setup/test-connection/{deviceId} handlers.( POST /setup/test-dns/{deviceId} handlers.(*Server).HandleTestDNSRedirection-fm POST /setup/test-hosts/{deviceId} handlers.(*Server).HandleTestHostsRedirection-fm POST /setup/trust-ca/{deviceId} handlers.(*Server).HandleTrustCACert-fm +POST /setup/tts/speak handlers.(*Server).HandleTTSSpeak-fm POST /streaming/account handlers.(*Server).HandleMargeCreateAccount-fm POST /streaming/account/login handlers.(*Server).HandleMargeLogin-fm POST /streaming/account/{account}/device/ handlers.(*Server).HandleMargeAddDevice-fm diff --git a/cmd/soundtouch-web/main.go b/cmd/soundtouch-web/main.go index 87eccc7..f887cad 100644 --- a/cmd/soundtouch-web/main.go +++ b/cmd/soundtouch-web/main.go @@ -81,17 +81,6 @@ func main() { Usage: "AfterTouch service base URL (e.g. https://soundtouch.local). Required for custom stream URLs to work as presets via LOCAL_INTERNET_RADIO", EnvVars: []string{"SERVICE_URL"}, }, - &cli.StringFlag{ - Name: "mgmt-username", - Usage: "AfterTouch management API username, used to proxy TTS to the service's /mgmt endpoints", - Value: "admin", - EnvVars: []string{"MGMT_USERNAME"}, - }, - &cli.StringFlag{ - Name: "mgmt-password", - Usage: "AfterTouch management API password, used to proxy TTS to the service's /mgmt endpoints", - EnvVars: []string{"MGMT_PASSWORD"}, - }, }, Action: func(c *cli.Context) error { port := c.String("port") @@ -126,8 +115,6 @@ func main() { webApp.Date = date webApp.RepoURL = repoURL webApp.ServiceURL = strings.TrimRight(c.String("service-url"), "/") - webApp.MgmtUsername = c.String("mgmt-username") - webApp.MgmtPassword = c.String("mgmt-password") discoveryService := soundtouchweb.NewDiscoveryService(ifaceName) diff --git a/docs/content/docs/reference/SPEAKER-ENDPOINT.md b/docs/content/docs/reference/SPEAKER-ENDPOINT.md index d9a1632..41e2294 100644 --- a/docs/content/docs/reference/SPEAKER-ENDPOINT.md +++ b/docs/content/docs/reference/SPEAKER-ENDPOINT.md @@ -308,10 +308,11 @@ soundtouch-service ### Triggering speech -Service management API (Basic Auth): +Service HTTP API (under `/setup`, LAN-trust like the rest of the setup surface, +no auth): ```bash -curl -u admin:change_me! -X POST http://soundtouch.local:8000/mgmt/tts/speak \ +curl -X POST http://soundtouch.local:8000/setup/tts/speak \ -H 'Content-Type: application/json' \ -d '{"host":"192.0.2.100","text":"Dinner is ready"}' ``` @@ -328,9 +329,9 @@ soundtouch-cli tts speak \ --text "Dinner is ready" ``` -Web UI: the per-device controls include a "Say something…" box. soundtouch-web -proxies it to the service, so start it with `--service-url` (and `--mgmt-username` -/ `--mgmt-password` if you changed the defaults). +Web UI: the TTS source view (and the Play URL view) include a "Say something…" +box. soundtouch-web proxies it to the service, so start it with `--service-url` +(or enter the service URL in the view). ### Notes and limitations diff --git a/pkg/service/handlers/handlers_tts.go b/pkg/service/handlers/handlers_tts.go index fd1b62e..5eca64d 100644 --- a/pkg/service/handlers/handlers_tts.go +++ b/pkg/service/handlers/handlers_tts.go @@ -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() diff --git a/pkg/service/handlers/handlers_tts_test.go b/pkg/service/handlers/handlers_tts_test.go index 9b779bb..0844fc3 100644 --- a/pkg/service/handlers/handlers_tts_test.go +++ b/pkg/service/handlers/handlers_tts_test.go @@ -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) diff --git a/pkg/service/soundtouchweb/handler.go b/pkg/service/soundtouchweb/handler.go index e4a1bd7..5ff6b60 100644 --- a/pkg/service/soundtouchweb/handler.go +++ b/pkg/service/soundtouchweb/handler.go @@ -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 } diff --git a/pkg/service/soundtouchweb/handlers_tts.go b/pkg/service/soundtouchweb/handlers_tts.go index 3af2f10..5fe9c1b 100644 --- a/pkg/service/soundtouchweb/handlers_tts.go +++ b/pkg/service/soundtouchweb/handlers_tts.go @@ -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) diff --git a/pkg/service/soundtouchweb/mount.go b/pkg/service/soundtouchweb/mount.go index 2f4e5b4..c4ac11c 100644 --- a/pkg/service/soundtouchweb/mount.go +++ b/pkg/service/soundtouchweb/mount.go @@ -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