diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go index 1f0feeb..7a53c69 100644 --- a/cmd/soundtouch-service/main.go +++ b/cmd/soundtouch-service/main.go @@ -658,10 +658,10 @@ func setupRouter(server *handlers.Server) *chi.Mux { }) r.Route("/mgmt", func(r chi.Router) { - // OAuth callback — no auth required (browser redirect from Spotify). - // The authorization code is single-use, short-lived, and useless without - // the client_secret (which only the server has). - r.Get("/spotify/confirm", server.HandleMgmtSpotifyConfirm) + // Browser OAuth callback — no auth required (Spotify redirects the + // user's browser here directly). The authorization code is single-use, + // short-lived, and useless without the client_secret. + r.Get("/spotify/callback", server.HandleMgmtSpotifyCallback) // All other management endpoints require Basic Auth. r.Group(func(r chi.Router) { @@ -669,6 +669,7 @@ func setupRouter(server *handlers.Server) *chi.Mux { r.Get("/accounts/{accountId}/speakers", server.HandleMgmtListSpeakers) r.Get("/devices/{deviceId}/events", server.HandleMgmtDeviceEvents) r.Post("/spotify/init", server.HandleMgmtSpotifyInit) + r.Post("/spotify/confirm", server.HandleMgmtSpotifyConfirm) r.Get("/spotify/accounts", server.HandleMgmtSpotifyAccounts) r.Get("/spotify/token", server.HandleMgmtSpotifyToken) r.Post("/spotify/entity", server.HandleMgmtSpotifyEntity) diff --git a/pkg/service/handlers/handlers_mgmt.go b/pkg/service/handlers/handlers_mgmt.go index fe3e242..0c29d9f 100644 --- a/pkg/service/handlers/handlers_mgmt.go +++ b/pkg/service/handlers/handlers_mgmt.go @@ -103,7 +103,51 @@ func (s *Server) HandleMgmtSpotifyInit(w http.ResponseWriter, _ *http.Request) { }) } +// HandleMgmtSpotifyCallback is the browser OAuth callback from Spotify. +// Not protected by Basic Auth — Spotify redirects the user's browser here directly. +// Returns an HTML page the user can close. +func (s *Server) HandleMgmtSpotifyCallback(w http.ResponseWriter, r *http.Request) { + s.mu.RLock() + svc := s.spotifyService + s.mu.RUnlock() + + if svc == nil { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusServiceUnavailable) + _, _ = w.Write([]byte(`
Spotify integration not configured
`)) + return + } + + if errMsg := r.URL.Query().Get("error"); errMsg != "" { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`Error: ` + errMsg + `
`)) + return + } + + code := r.URL.Query().Get("code") + if code == "" { + w.Header().Set("Content-Type", "text/html") + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(`Token exchange failed
`)) + return + } + + w.Header().Set("Content-Type", "text/html") + _, _ = w.Write([]byte(`You can close this window.
`)) +} + // HandleMgmtSpotifyConfirm exchanges an authorization code for tokens. +// Used by the ueberboese mobile app after the deep link callback delivers the code. +// Protected by Basic Auth. func (s *Server) HandleMgmtSpotifyConfirm(w http.ResponseWriter, r *http.Request) { s.mu.RLock() svc := s.spotifyService