diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-07 00:20:15 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-07 00:20:15 +0300 |
| commit | d87bcee585883f22b2ca0b42db67bf815cc7c1fc (patch) | |
| tree | a879c345d3ebaecf526f12d7ad6cf97c6f7f4dba /internal/api | |
| parent | e1bea8d74391a20201327299481550bf94766236 (diff) | |
Task 51: add API error helpers
Diffstat (limited to 'internal/api')
| -rw-r--r-- | internal/api/handlers.go | 16 | ||||
| -rw-r--r-- | internal/api/handlers_admin.go | 8 | ||||
| -rw-r--r-- | internal/api/handlers_auth.go | 10 | ||||
| -rw-r--r-- | internal/api/handlers_file.go | 14 | ||||
| -rw-r--r-- | internal/api/handlers_media.go | 66 | ||||
| -rw-r--r-- | internal/api/handlers_podcast.go | 24 | ||||
| -rw-r--r-- | internal/api/handlers_share.go | 6 |
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 { |
