summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-07 00:20:15 +0300
committerPaul Buetow <paul@buetow.org>2026-05-07 00:20:15 +0300
commitd87bcee585883f22b2ca0b42db67bf815cc7c1fc (patch)
treea879c345d3ebaecf526f12d7ad6cf97c6f7f4dba /internal/api
parente1bea8d74391a20201327299481550bf94766236 (diff)
Task 51: add API error helpers
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/handlers.go16
-rw-r--r--internal/api/handlers_admin.go8
-rw-r--r--internal/api/handlers_auth.go10
-rw-r--r--internal/api/handlers_file.go14
-rw-r--r--internal/api/handlers_media.go66
-rw-r--r--internal/api/handlers_podcast.go24
-rw-r--r--internal/api/handlers_share.go6
7 files changed, 80 insertions, 64 deletions
diff --git a/internal/api/handlers.go b/internal/api/handlers.go
index fcda820..b837a18 100644
--- a/internal/api/handlers.go
+++ b/internal/api/handlers.go
@@ -25,6 +25,22 @@ func writeJSON(w http.ResponseWriter, status int, data interface{}) {
}
}
+func writeError(w http.ResponseWriter, status int, message string) {
+ writeJSON(w, status, map[string]string{"error": message})
+}
+
+func badRequest(w http.ResponseWriter, message string) {
+ writeError(w, http.StatusBadRequest, message)
+}
+
+func notFound(w http.ResponseWriter) {
+ writeError(w, http.StatusNotFound, "not found")
+}
+
+func forbidden(w http.ResponseWriter, message string) {
+ writeError(w, http.StatusForbidden, message)
+}
+
func readJSON(r *http.Request, dst interface{}) error {
if r.Body == nil {
return errors.New("missing body")
diff --git a/internal/api/handlers_admin.go b/internal/api/handlers_admin.go
index e9935d4..b1c3005 100644
--- a/internal/api/handlers_admin.go
+++ b/internal/api/handlers_admin.go
@@ -62,7 +62,7 @@ func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
IsAdmin bool `json:"is_admin"`
}
if err := readJSON(r, &req); err != nil || req.Username == "" || req.Password == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
+ badRequest(w, "invalid request")
return
}
user, err := s.adminSvc.CreateUser(r.Context(), req.Username, req.Password, req.IsAdmin)
@@ -80,7 +80,7 @@ func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
id := pathID(r, "id")
adminUser, _ := r.Context().Value(userCtxKey).(*model.User)
if adminUser != nil && adminUser.ID == id {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "cannot delete self"})
+ badRequest(w, "cannot delete self")
return
}
if err := s.adminSvc.DeleteUser(r.Context(), id); err != nil {
@@ -112,7 +112,7 @@ func (s *Server) handleGrantPermission(w http.ResponseWriter, r *http.Request) {
Role model.Role `json:"role"`
}
if err := readJSON(r, &req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
+ badRequest(w, "invalid request")
return
}
if err := s.adminSvc.GrantPermission(r.Context(), req.SetID, req.UserID, req.Role); err != nil {
@@ -131,7 +131,7 @@ func (s *Server) handleRevokePermission(w http.ResponseWriter, r *http.Request)
UserID int64 `json:"user_id"`
}
if err := readJSON(r, &req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
+ badRequest(w, "invalid request")
return
}
if err := s.adminSvc.RevokePermission(r.Context(), req.SetID, req.UserID); err != nil {
diff --git a/internal/api/handlers_auth.go b/internal/api/handlers_auth.go
index 5bf5f39..b0a1874 100644
--- a/internal/api/handlers_auth.go
+++ b/internal/api/handlers_auth.go
@@ -28,18 +28,18 @@ func (s *Server) handleBootstrap(w http.ResponseWriter, r *http.Request) {
}
var req bootstrapRequest
if err := readJSON(r, &req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
+ badRequest(w, "invalid request body")
return
}
if req.Username == "" || req.Password == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password required"})
+ badRequest(w, "username and password required")
return
}
res, err := s.authSvc.Bootstrap(r.Context(), req.Username, req.Password)
if err != nil {
if errors.Is(err, service.ErrAlreadyBootstrapped) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "bootstrap already complete"})
+ forbidden(w, "bootstrap already complete")
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
@@ -56,11 +56,11 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
}
var req loginRequest
if err := readJSON(r, &req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
+ badRequest(w, "invalid request body")
return
}
if req.Username == "" || req.Password == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password required"})
+ badRequest(w, "username and password required")
return
}
diff --git a/internal/api/handlers_file.go b/internal/api/handlers_file.go
index 0638603..386ec6e 100644
--- a/internal/api/handlers_file.go
+++ b/internal/api/handlers_file.go
@@ -53,24 +53,24 @@ func (s *Server) handleDownload(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
res, err := s.browseSvc.DownloadMedia(r.Context(), id, userIDFromContext(r))
if err != nil {
if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
+ forbidden(w, "forbidden")
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if res == nil {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
s.serveFileResult(w, r, res, true)
@@ -90,16 +90,16 @@ func (s *Server) handleRegenThumbnail(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
if err := s.writeSvc.RegenerateThumbnail(r.Context(), id, userIDFromContext(r)); err != nil {
if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
+ forbidden(w, "forbidden")
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
diff --git a/internal/api/handlers_media.go b/internal/api/handlers_media.go
index ee74d38..a2ac421 100644
--- a/internal/api/handlers_media.go
+++ b/internal/api/handlers_media.go
@@ -34,18 +34,18 @@ func (s *Server) handleGetSetCover(w http.ResponseWriter, r *http.Request) {
}
setID := pathID(r, "id")
if setID == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid set id"})
+ badRequest(w, "invalid set id")
return
}
folder := r.URL.Query().Get("folder")
fr, err := s.browseSvc.GetSetCover(r.Context(), setID, folder, userIDFromContext(r))
if err != nil {
if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
+ forbidden(w, "forbidden")
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -61,17 +61,17 @@ func (s *Server) handlePostSetCover(w http.ResponseWriter, r *http.Request) {
}
setID := pathID(r, "id")
if setID == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid set id"})
+ badRequest(w, "invalid set id")
return
}
folder := r.URL.Query().Get("folder")
if err := s.writeSvc.RegenerateSetCover(r.Context(), setID, folder, userIDFromContext(r)); err != nil {
if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
+ forbidden(w, "forbidden")
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -86,14 +86,14 @@ func (s *Server) handleBrowseSet(w http.ResponseWriter, r *http.Request) {
}
setID := pathID(r, "id")
if setID == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid set id"})
+ badRequest(w, "invalid set id")
return
}
parent := r.URL.Query().Get("parent")
result, err := s.browseSvc.BrowseSet(r.Context(), setID, userIDFromContext(r), parent)
if err != nil {
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
+ forbidden(w, "forbidden")
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -108,7 +108,7 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
}
setID := pathID(r, "id")
if setID == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid set id"})
+ badRequest(w, "invalid set id")
return
}
@@ -120,13 +120,13 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "file too large"})
return
}
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid multipart form"})
+ badRequest(w, "invalid multipart form")
return
}
file, fh, err := r.FormFile("file")
if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing file"})
+ badRequest(w, "missing file")
return
}
defer file.Close()
@@ -134,15 +134,15 @@ func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
media, err := s.writeSvc.UploadMedia(r.Context(), setID, userIDFromContext(r), fh.Filename, file, fh.Size)
if err != nil {
if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
+ forbidden(w, "forbidden")
return
}
if errors.Is(err, service.ErrUnsupportedExtension) {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
+ badRequest(w, err.Error())
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -252,7 +252,7 @@ func (s *Server) handleGetMedia(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
detail, err := s.browseSvc.GetMediaDetail(r.Context(), id, userIDFromContext(r))
@@ -261,7 +261,7 @@ func (s *Server) handleGetMedia(w http.ResponseWriter, r *http.Request) {
return
}
if detail == nil {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
writeJSON(w, http.StatusOK, detail)
@@ -273,7 +273,7 @@ func (s *Server) handleFavorite(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
fav, err := s.favSvc.ToggleFavorite(r.Context(), userIDFromContext(r), id)
@@ -290,14 +290,14 @@ func (s *Server) handleAddTag(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
var req struct {
Tag string `json:"tag"`
}
if err := readJSON(r, &req); err != nil || req.Tag == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "tag required"})
+ badRequest(w, "tag required")
return
}
if err := s.tagSvc.AssignTag(r.Context(), id, userIDFromContext(r), req.Tag); err != nil {
@@ -314,7 +314,7 @@ func (s *Server) handleRemoveTag(w http.ResponseWriter, r *http.Request) {
id := pathID(r, "id")
tagName := r.PathValue("tag")
if id == 0 || tagName == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid parameters"})
+ badRequest(w, "invalid parameters")
return
}
if err := s.tagSvc.RemoveTag(r.Context(), id, userIDFromContext(r), tagName); err != nil {
@@ -330,16 +330,16 @@ func (s *Server) handleSoftDelete(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
if err := s.writeSvc.SoftDeleteMedia(r.Context(), id, userIDFromContext(r)); err != nil {
if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
+ forbidden(w, "forbidden")
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -354,16 +354,16 @@ func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
if err := s.writeSvc.RestoreMedia(r.Context(), id, userIDFromContext(r)); err != nil {
if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
+ forbidden(w, "forbidden")
return
}
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
@@ -382,7 +382,7 @@ func (s *Server) handleGetNote(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
note, err := s.noteSvc.GetNote(r.Context(), id, userIDFromContext(r))
@@ -403,14 +403,14 @@ func (s *Server) handleUpsertNote(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
var req struct {
Content string `json:"content"`
}
if err := readJSON(r, &req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid body"})
+ badRequest(w, "invalid body")
return
}
note := &model.Note{MediaID: id, UserID: userIDFromContext(r), Content: req.Content}
@@ -427,7 +427,7 @@ func (s *Server) handleDeleteNote(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
if err := s.noteSvc.DeleteNote(r.Context(), id, userIDFromContext(r)); err != nil {
@@ -450,16 +450,16 @@ func (s *Server) handleProgress(w http.ResponseWriter, r *http.Request) {
Position float64 `json:"position_seconds"`
}
if err := readJSON(r, &req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid body"})
+ badRequest(w, "invalid body")
return
}
if req.MediaID == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "media_id required"})
+ badRequest(w, "media_id required")
return
}
sessionID := sessionIDFromContext(r)
if sessionID == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "session required"})
+ badRequest(w, "session required")
return
}
err := s.progressSvc.UpdateProgress(
diff --git a/internal/api/handlers_podcast.go b/internal/api/handlers_podcast.go
index 43f724c..4568022 100644
--- a/internal/api/handlers_podcast.go
+++ b/internal/api/handlers_podcast.go
@@ -44,18 +44,18 @@ func (s *Server) handleSubscribePodcast(w http.ResponseWriter, r *http.Request)
SetName string `json:"set_name"`
}
if err := readJSON(r, &req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
+ badRequest(w, "invalid request")
return
}
if req.FeedURL == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "feed_url is required"})
+ badRequest(w, "feed_url is required")
return
}
feed, err := s.podcastSvc.SubscribeFeed(r.Context(), req.FeedURL, req.SetName, userIDFromContext(r))
if err != nil {
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"})
+ forbidden(w, "access denied")
return
}
s.logger.Error("subscribe podcast", "err", err)
@@ -73,7 +73,7 @@ func (s *Server) handleListEpisodes(w http.ResponseWriter, r *http.Request) {
setID := pathID(r, "id")
if setID == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid set id"})
+ badRequest(w, "invalid set id")
return
}
@@ -91,11 +91,11 @@ func (s *Server) handleListEpisodes(w http.ResponseWriter, r *http.Request) {
episodes, err := s.podcastSvc.ListEpisodes(r.Context(), setID, userIDFromContext(r), limit, offset)
if err != nil {
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"})
+ forbidden(w, "access denied")
return
}
if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
s.logger.Error("list episodes", "err", err)
@@ -113,18 +113,18 @@ func (s *Server) handleDownloadEpisode(w http.ResponseWriter, r *http.Request) {
episodeID := pathID(r, "episode_id")
if episodeID == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid episode id"})
+ badRequest(w, "invalid episode id")
return
}
media, err := s.podcastSvc.DownloadEpisode(r.Context(), episodeID, userIDFromContext(r))
if err != nil {
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"})
+ forbidden(w, "access denied")
return
}
if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
s.logger.Error("download episode", "err", err)
@@ -142,17 +142,17 @@ func (s *Server) handleToggleComplete(w http.ResponseWriter, r *http.Request) {
episodeID := pathID(r, "episode_id")
if episodeID == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid episode id"})
+ badRequest(w, "invalid episode id")
return
}
if err := s.podcastSvc.ToggleEpisodeComplete(r.Context(), episodeID, userIDFromContext(r)); err != nil {
if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "access denied"})
+ forbidden(w, "access denied")
return
}
if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
+ notFound(w)
return
}
s.logger.Error("toggle complete", "err", err)
diff --git a/internal/api/handlers_share.go b/internal/api/handlers_share.go
index ed6ae20..54ce60b 100644
--- a/internal/api/handlers_share.go
+++ b/internal/api/handlers_share.go
@@ -22,7 +22,7 @@ func (s *Server) handleCreateShare(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
expiresAt := time.Now().Add(time.Duration(s.cfg.ShareDefaultExpiryDays) * 24 * time.Hour)
@@ -40,7 +40,7 @@ func (s *Server) handleListShares(w http.ResponseWriter, r *http.Request) {
}
id := pathID(r, "id")
if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
+ badRequest(w, "invalid media id")
return
}
shares, err := s.shareSvc.ListShares(r.Context(), id, userIDFromContext(r))
@@ -57,7 +57,7 @@ func (s *Server) handleRevokeShare(w http.ResponseWriter, r *http.Request) {
}
token := r.PathValue("token")
if token == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "token required"})
+ badRequest(w, "token required")
return
}
if err := s.shareSvc.RevokeShare(r.Context(), token, userIDFromContext(r)); err != nil {