feat(api): /api/quarantine user + admin endpoints + Service wiring
User-facing /api/quarantine handlers (POST flag, DELETE unflag, GET mine) plus the five /api/admin/quarantine endpoints (queue, resolve, delete-file, delete-via-lidarr, actions). Mount() and the handlers struct now accept the lidarrquarantine.Service, constructed in server.go alongside the existing lidarrrequests wiring.
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrquarantine"
|
||||
)
|
||||
|
||||
// quarantineView is the JSON shape returned by the user-facing endpoints
|
||||
// that operate on a single quarantine row (POST /api/quarantine).
|
||||
type quarantineView struct {
|
||||
UserID string `json:"user_id"`
|
||||
TrackID string `json:"track_id"`
|
||||
Reason string `json:"reason"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
func quarantineViewFrom(row dbq.LidarrQuarantine) quarantineView {
|
||||
return quarantineView{
|
||||
UserID: uuidToString(row.UserID),
|
||||
TrackID: uuidToString(row.TrackID),
|
||||
Reason: string(row.Reason),
|
||||
Notes: row.Notes,
|
||||
CreatedAt: row.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// flagBody is the JSON body for POST /api/quarantine. track_id is sent as
|
||||
// the canonical hyphenated string the SPA already gets from /api/tracks/*.
|
||||
type flagBody struct {
|
||||
TrackID string `json:"track_id"`
|
||||
Reason string `json:"reason"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
// handleFlag implements POST /api/quarantine. Upserts via Service.Flag.
|
||||
func (h *handlers) handleFlag(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
if !ok {
|
||||
writeErr(w, http.StatusUnauthorized, "unauthorized", "authentication required")
|
||||
return
|
||||
}
|
||||
var body flagBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, "bad_request", "invalid JSON body")
|
||||
return
|
||||
}
|
||||
trackID, ok := parseUUID(body.TrackID)
|
||||
if !ok {
|
||||
writeErr(w, http.StatusBadRequest, "bad_request", "invalid track id")
|
||||
return
|
||||
}
|
||||
row, err := h.lidarrQuarantine.Flag(r.Context(), user.ID, trackID, body.Reason, body.Notes)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, lidarrquarantine.ErrBadReason):
|
||||
writeErr(w, http.StatusBadRequest, "bad_reason", "invalid reason")
|
||||
case errors.Is(err, lidarrquarantine.ErrTrackNotFound):
|
||||
writeErr(w, http.StatusNotFound, "track_not_found", "track does not exist")
|
||||
default:
|
||||
h.logger.Error("api: flag", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "flag failed")
|
||||
}
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusCreated, quarantineViewFrom(row))
|
||||
}
|
||||
|
||||
// handleUnflag implements DELETE /api/quarantine/{track_id}.
|
||||
func (h *handlers) handleUnflag(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
if !ok {
|
||||
writeErr(w, http.StatusUnauthorized, "unauthorized", "authentication required")
|
||||
return
|
||||
}
|
||||
id, ok := parseUUID(chi.URLParam(r, "track_id"))
|
||||
if !ok {
|
||||
writeErr(w, http.StatusBadRequest, "bad_request", "invalid track id")
|
||||
return
|
||||
}
|
||||
if err := h.lidarrQuarantine.Unflag(r.Context(), user.ID, id); err != nil {
|
||||
if errors.Is(err, lidarrquarantine.ErrQuarantineNotFound) {
|
||||
writeErr(w, http.StatusNotFound, "quarantine_not_found", "no quarantine for that track")
|
||||
return
|
||||
}
|
||||
h.logger.Error("api: unflag", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "unflag failed")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// quarantineMineView is the per-row shape returned by GET /api/quarantine/mine,
|
||||
// composed from the joined ListQuarantineForUserRow.
|
||||
type quarantineMineView struct {
|
||||
TrackID string `json:"track_id"`
|
||||
Reason string `json:"reason"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
TrackTitle string `json:"track_title"`
|
||||
TrackDurationMs int32 `json:"track_duration_ms"`
|
||||
AlbumID string `json:"album_id"`
|
||||
AlbumTitle string `json:"album_title"`
|
||||
AlbumCoverArtPath *string `json:"album_cover_art_path,omitempty"`
|
||||
ArtistID string `json:"artist_id"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
}
|
||||
|
||||
// handleListMyQuarantine implements GET /api/quarantine/mine. Returns the
|
||||
// caller's quarantines with track/album/artist detail.
|
||||
func (h *handlers) handleListMyQuarantine(w http.ResponseWriter, r *http.Request) {
|
||||
user, ok := auth.UserFromContext(r.Context())
|
||||
if !ok {
|
||||
writeErr(w, http.StatusUnauthorized, "unauthorized", "authentication required")
|
||||
return
|
||||
}
|
||||
rows, err := h.lidarrQuarantine.ListMine(r.Context(), user.ID)
|
||||
if err != nil {
|
||||
h.logger.Error("api: list mine", "err", err)
|
||||
writeErr(w, http.StatusInternalServerError, "server_error", "list failed")
|
||||
return
|
||||
}
|
||||
out := make([]quarantineMineView, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
out = append(out, quarantineMineView{
|
||||
TrackID: uuidToString(row.LidarrQuarantine.TrackID),
|
||||
Reason: string(row.LidarrQuarantine.Reason),
|
||||
Notes: row.LidarrQuarantine.Notes,
|
||||
CreatedAt: row.LidarrQuarantine.CreatedAt,
|
||||
TrackTitle: row.TrackTitle,
|
||||
TrackDurationMs: row.TrackDurationMs,
|
||||
AlbumID: uuidToString(row.AlbumID),
|
||||
AlbumTitle: row.AlbumTitle,
|
||||
AlbumCoverArtPath: row.AlbumCoverArtPath,
|
||||
ArtistID: uuidToString(row.ArtistID),
|
||||
ArtistName: row.ArtistName,
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
Reference in New Issue
Block a user