From 8cd8825efcc280a1d903b1c0683ef41de81a6bf6 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 8 May 2026 08:20:33 -0400 Subject: [PATCH] refactor(server/api): migrate likes.go + playlists.go preludes (A2 2/N) --- internal/api/likes.go | 51 ++++++++++--------------------- internal/api/playlists.go | 63 +++++++++++++-------------------------- 2 files changed, 36 insertions(+), 78 deletions(-) diff --git a/internal/api/likes.go b/internal/api/likes.go index beaf60b6..b94d2ae4 100644 --- a/internal/api/likes.go +++ b/internal/api/likes.go @@ -1,16 +1,13 @@ package api import ( - "encoding/json" "errors" "net/http" - "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" - "git.fabledsword.com/bvandeusen/minstrel/internal/auth" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/playevents" ) @@ -22,14 +19,12 @@ type likedIDsResponse struct { } func (h *handlers) handleLikeTrack(w http.ResponseWriter, r *http.Request) { - user, ok := auth.UserFromContext(r.Context()) + user, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.ErrUnauthorized) return } - id, ok := parseUUID(chi.URLParam(r, "id")) + id, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid track id")) return } q := dbq.New(h.pool) @@ -55,14 +50,12 @@ func (h *handlers) handleLikeTrack(w http.ResponseWriter, r *http.Request) { } func (h *handlers) handleUnlikeTrack(w http.ResponseWriter, r *http.Request) { - user, ok := auth.UserFromContext(r.Context()) + user, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.ErrUnauthorized) return } - id, ok := parseUUID(chi.URLParam(r, "id")) + id, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid track id")) return } q := dbq.New(h.pool) @@ -79,14 +72,12 @@ func (h *handlers) handleUnlikeTrack(w http.ResponseWriter, r *http.Request) { } func (h *handlers) handleLikeAlbum(w http.ResponseWriter, r *http.Request) { - user, ok := auth.UserFromContext(r.Context()) + user, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.ErrUnauthorized) return } - id, ok := parseUUID(chi.URLParam(r, "id")) + id, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid album id")) return } q := dbq.New(h.pool) @@ -108,14 +99,12 @@ func (h *handlers) handleLikeAlbum(w http.ResponseWriter, r *http.Request) { } func (h *handlers) handleUnlikeAlbum(w http.ResponseWriter, r *http.Request) { - user, ok := auth.UserFromContext(r.Context()) + user, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.ErrUnauthorized) return } - id, ok := parseUUID(chi.URLParam(r, "id")) + id, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid album id")) return } if err := dbq.New(h.pool).UnlikeAlbum(r.Context(), dbq.UnlikeAlbumParams{UserID: user.ID, AlbumID: id}); err != nil { @@ -127,14 +116,12 @@ func (h *handlers) handleUnlikeAlbum(w http.ResponseWriter, r *http.Request) { } func (h *handlers) handleLikeArtist(w http.ResponseWriter, r *http.Request) { - user, ok := auth.UserFromContext(r.Context()) + user, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.ErrUnauthorized) return } - id, ok := parseUUID(chi.URLParam(r, "id")) + id, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid artist id")) return } q := dbq.New(h.pool) @@ -156,14 +143,12 @@ func (h *handlers) handleLikeArtist(w http.ResponseWriter, r *http.Request) { } func (h *handlers) handleUnlikeArtist(w http.ResponseWriter, r *http.Request) { - user, ok := auth.UserFromContext(r.Context()) + user, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.ErrUnauthorized) return } - id, ok := parseUUID(chi.URLParam(r, "id")) + id, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid artist id")) return } if err := dbq.New(h.pool).UnlikeArtist(r.Context(), dbq.UnlikeArtistParams{UserID: user.ID, ArtistID: id}); err != nil { @@ -175,9 +160,8 @@ func (h *handlers) handleUnlikeArtist(w http.ResponseWriter, r *http.Request) { } func (h *handlers) handleListLikedTracks(w http.ResponseWriter, r *http.Request) { - user, ok := auth.UserFromContext(r.Context()) + user, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.ErrUnauthorized) return } limit, offset, err := parsePaging(r.URL.Query()) @@ -209,9 +193,8 @@ func (h *handlers) handleListLikedTracks(w http.ResponseWriter, r *http.Request) } func (h *handlers) handleListLikedAlbums(w http.ResponseWriter, r *http.Request) { - user, ok := auth.UserFromContext(r.Context()) + user, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.ErrUnauthorized) return } limit, offset, err := parsePaging(r.URL.Query()) @@ -243,9 +226,8 @@ func (h *handlers) handleListLikedAlbums(w http.ResponseWriter, r *http.Request) } func (h *handlers) handleListLikedArtists(w http.ResponseWriter, r *http.Request) { - user, ok := auth.UserFromContext(r.Context()) + user, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.ErrUnauthorized) return } limit, offset, err := parsePaging(r.URL.Query()) @@ -282,9 +264,8 @@ func (h *handlers) handleListLikedArtists(w http.ResponseWriter, r *http.Request } func (h *handlers) handleGetLikedIDs(w http.ResponseWriter, r *http.Request) { - user, ok := auth.UserFromContext(r.Context()) + user, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.ErrUnauthorized) return } q := dbq.New(h.pool) diff --git a/internal/api/playlists.go b/internal/api/playlists.go index b6221c9d..03536cda 100644 --- a/internal/api/playlists.go +++ b/internal/api/playlists.go @@ -2,7 +2,6 @@ package api import ( "context" - "encoding/json" "errors" "fmt" "net/http" @@ -11,12 +10,10 @@ import ( "strings" "time" - "github.com/go-chi/chi/v5" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "git.fabledsword.com/bvandeusen/minstrel/internal/apierror" - "git.fabledsword.com/bvandeusen/minstrel/internal/auth" "git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq" "git.fabledsword.com/bvandeusen/minstrel/internal/playlists" ) @@ -88,9 +85,8 @@ type listPlaylistsResponse struct { // The Public bucket (other users' public playlists) is always unfiltered // since system mixes are always private. func (h *handlers) handleListPlaylists(w http.ResponseWriter, r *http.Request) { - caller, ok := auth.UserFromContext(r.Context()) + caller, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.Unauthorized("unauthenticated", "no session")) return } @@ -175,14 +171,12 @@ type createPlaylistBody struct { // handleCreatePlaylist implements POST /api/playlists. Empty Name is // rejected by the service (ErrInvalidInput → 400 bad_request). func (h *handlers) handleCreatePlaylist(w http.ResponseWriter, r *http.Request) { - caller, ok := auth.UserFromContext(r.Context()) + caller, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.Unauthorized("unauthenticated", "no session")) return } var body createPlaylistBody - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - writeErr(w, apierror.BadRequest("bad_request", "invalid JSON body")) + if !decodeBody(w, r, &body) { return } row, err := h.playlists.Create(r.Context(), caller.ID, body.Name, body.Description, body.IsPublic) @@ -197,14 +191,12 @@ func (h *handlers) handleCreatePlaylist(w http.ResponseWriter, r *http.Request) // owner-or-public; the service returns ErrForbidden when a non-owner // asks for a private playlist. func (h *handlers) handleGetPlaylist(w http.ResponseWriter, r *http.Request) { - caller, ok := auth.UserFromContext(r.Context()) + caller, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.Unauthorized("unauthenticated", "no session")) return } - playlistID, ok := parseUUID(chi.URLParam(r, "id")) + playlistID, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid playlist id")) return } detail, err := h.playlists.Get(r.Context(), caller.ID, playlistID) @@ -226,19 +218,16 @@ type updatePlaylistBody struct { // handleUpdatePlaylist implements PATCH /api/playlists/{id}. Owner only; // non-owners get 403 not_authorized. func (h *handlers) handleUpdatePlaylist(w http.ResponseWriter, r *http.Request) { - caller, ok := auth.UserFromContext(r.Context()) + caller, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.Unauthorized("unauthenticated", "no session")) return } - playlistID, ok := parseUUID(chi.URLParam(r, "id")) + playlistID, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid playlist id")) return } var body updatePlaylistBody - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - writeErr(w, apierror.BadRequest("bad_request", "invalid JSON body")) + if !decodeBody(w, r, &body) { return } row, err := h.playlists.Update(r.Context(), caller.ID, playlistID, playlists.UpdateInput{ @@ -256,14 +245,12 @@ func (h *handlers) handleUpdatePlaylist(w http.ResponseWriter, r *http.Request) // handleDeletePlaylist implements DELETE /api/playlists/{id}. Owner only; // returns 204 on success. func (h *handlers) handleDeletePlaylist(w http.ResponseWriter, r *http.Request) { - caller, ok := auth.UserFromContext(r.Context()) + caller, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.Unauthorized("unauthenticated", "no session")) return } - playlistID, ok := parseUUID(chi.URLParam(r, "id")) + playlistID, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid playlist id")) return } if err := h.playlists.Delete(r.Context(), caller.ID, playlistID); err != nil { @@ -281,19 +268,16 @@ type appendTracksBody struct { // only. Returns the playlist detail (with the new track rows) so the // SPA never has to re-GET after a mutation. func (h *handlers) handleAppendTracks(w http.ResponseWriter, r *http.Request) { - caller, ok := auth.UserFromContext(r.Context()) + caller, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.Unauthorized("unauthenticated", "no session")) return } - playlistID, ok := parseUUID(chi.URLParam(r, "id")) + playlistID, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid playlist id")) return } var body appendTracksBody - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - writeErr(w, apierror.BadRequest("bad_request", "invalid JSON body")) + if !decodeBody(w, r, &body) { return } trackIDs := make([]pgtype.UUID, 0, len(body.TrackIDs)) @@ -320,14 +304,12 @@ func (h *handlers) handleAppendTracks(w http.ResponseWriter, r *http.Request) { // handleRemovePlaylistTrack implements DELETE /api/playlists/{id}/tracks/{position}. // Owner only. Returns the post-mutation detail like AppendTracks. func (h *handlers) handleRemovePlaylistTrack(w http.ResponseWriter, r *http.Request) { - caller, ok := auth.UserFromContext(r.Context()) + caller, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.Unauthorized("unauthenticated", "no session")) return } - playlistID, ok := parseUUID(chi.URLParam(r, "id")) + playlistID, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid playlist id")) return } posStr := chi.URLParam(r, "position") @@ -356,19 +338,16 @@ type reorderTracksBody struct { // only. The body carries a permutation of every existing position; the // service validates length and uniqueness. func (h *handlers) handleReorderPlaylist(w http.ResponseWriter, r *http.Request) { - caller, ok := auth.UserFromContext(r.Context()) + caller, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.Unauthorized("unauthenticated", "no session")) return } - playlistID, ok := parseUUID(chi.URLParam(r, "id")) + playlistID, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid playlist id")) return } var body reorderTracksBody - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - writeErr(w, apierror.BadRequest("bad_request", "invalid JSON body")) + if !decodeBody(w, r, &body) { return } if err := h.playlists.Reorder(r.Context(), caller.ID, playlistID, body.OrderedPositions); err != nil { @@ -388,14 +367,12 @@ func (h *handlers) handleReorderPlaylist(w http.ResponseWriter, r *http.Request) // call before we look at cover_path, so we don't leak the existence of // private playlists to non-owners. func (h *handlers) handleGetPlaylistCover(w http.ResponseWriter, r *http.Request) { - caller, ok := auth.UserFromContext(r.Context()) + caller, ok := requireUser(w, r) if !ok { - writeErr(w, apierror.Unauthorized("unauthenticated", "no session")) return } - playlistID, ok := parseUUID(chi.URLParam(r, "id")) + playlistID, ok := requireURLUUID(w, r, "id") if !ok { - writeErr(w, apierror.BadRequest("bad_request", "invalid playlist id")) return } detail, err := h.playlists.Get(r.Context(), caller.ID, playlistID)