summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-01 21:10:43 +0300
committerPaul Buetow <paul@buetow.org>2026-05-01 21:10:43 +0300
commitaf29deb33ee25800976b7122236bf7895a5ff39e (patch)
treeb138a6dfe2bb9636ff1c58ff8d255223cd145901
parent74c2e0982f336ab135d8b63d26fa49bcd58e57a9 (diff)
refactor(api): split monolithic handlers.go into domain-specific files (task 3)
Split internal/api/handlers.go (1045 lines) to improve KISS/SRP: - handlers_auth.go – bootstrap, login, logout, health, session cookies - handlers_media.go – sets, media CRUD, tags, favorites, notes, progress - handlers_share.go – create/list/revoke shares, share page, share stream - handlers_admin.go – trash, rescan, users, permissions - handlers_file.go – stream, download, thumbnail, regenerate thumbnail Shared helpers (writeJSON, readJSON, pathID, serveFileResult, mimeTypeForFilename, etc.) remain in handlers.go. All tests pass: go test ./... -race -cover.
-rw-r--r--internal/api/handlers.go882
-rw-r--r--internal/api/handlers_admin.go135
-rw-r--r--internal/api/handlers_auth.go148
-rw-r--r--internal/api/handlers_file.go108
-rw-r--r--internal/api/handlers_media.go421
-rw-r--r--internal/api/handlers_share.go116
6 files changed, 931 insertions, 879 deletions
diff --git a/internal/api/handlers.go b/internal/api/handlers.go
index 433de7d..8573857 100644
--- a/internal/api/handlers.go
+++ b/internal/api/handlers.go
@@ -1,35 +1,24 @@
package api
import (
- "context"
"encoding/json"
- "errors"
"fmt"
"io"
"log/slog"
"mime"
"net/http"
- "net/url"
"os"
"path/filepath"
"strconv"
"strings"
- "time"
"codeberg.org/snonux/player/internal/model"
- "codeberg.org/snonux/player/internal/repository"
"codeberg.org/snonux/player/internal/service"
)
-type bootstrapRequest struct {
- Username string `json:"username"`
- Password string `json:"password"`
-}
-
-type loginRequest struct {
- Username string `json:"username"`
- Password string `json:"password"`
-}
+// ------------------------------------------------------------------
+// Helpers
+// ------------------------------------------------------------------
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
@@ -114,443 +103,6 @@ func (s *Server) serveBootstrap(w http.ResponseWriter, r *http.Request) {
}
// ------------------------------------------------------------------
-// Bootstrap & Auth
-// ------------------------------------------------------------------
-
-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"})
- return
- }
- if req.Username == "" || req.Password == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password required"})
- return
- }
-
- ctx := r.Context()
- count, err := s.store.CountUsers(ctx)
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
- return
- }
- if count > 0 {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "bootstrap already complete"})
- return
- }
-
- hash, err := s.hasher.Hash(req.Password)
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
- return
- }
-
- user := &model.User{Username: req.Username, PasswordHash: hash, IsAdmin: true, CreatedAt: time.Now()}
- id, err := s.store.CreateUser(ctx, user)
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
- return
- }
- user.ID = id
-
- sessID, err := s.sm.CreateSession(ctx, id)
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
- return
- }
- s.setSessionCookie(w, sessID)
- writeJSON(w, http.StatusOK, map[string]interface{}{"id": user.ID, "username": user.Username, "is_admin": user.IsAdmin})
-}
-
-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"})
- return
- }
- if req.Username == "" || req.Password == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "username and password required"})
- return
- }
-
- ctx := r.Context()
- user, err := s.store.GetUserByUsername(ctx, req.Username)
- if err != nil {
- writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
- return
- }
- if user == nil {
- writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
- return
- }
- if err := s.hasher.Compare(user.PasswordHash, req.Password); err != nil {
- writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
- return
- }
-
- sessID, err := s.sm.CreateSession(ctx, user.ID)
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal server error"})
- return
- }
- s.setSessionCookie(w, sessID)
- writeJSON(w, http.StatusOK, map[string]interface{}{"id": user.ID, "username": user.Username, "is_admin": user.IsAdmin})
-}
-
-func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) {
- cookie, err := r.Cookie("session")
- if err == nil && cookie.Value != "" {
- _ = s.sm.DeleteSession(r.Context(), cookie.Value)
- }
- s.clearSessionCookie(w)
- w.WriteHeader(http.StatusNoContent)
-}
-
-func (s *Server) handleHealthz(w http.ResponseWriter, r *http.Request) {
- w.WriteHeader(http.StatusOK)
-}
-
-func (s *Server) handleReadyz(w http.ResponseWriter, r *http.Request) {
- if err := s.pingStore(r.Context()); err != nil {
- http.Error(w, "not ready", http.StatusServiceUnavailable)
- return
- }
- w.WriteHeader(http.StatusOK)
-}
-
-func (s *Server) setSessionCookie(w http.ResponseWriter, value string) {
- http.SetCookie(w, &http.Cookie{
- Name: "session",
- Value: value,
- Path: "/",
- HttpOnly: true,
- Secure: s.cfg.SecureCookies,
- SameSite: http.SameSiteStrictMode,
- Expires: time.Now().Add(time.Duration(s.cfg.SessionTimeoutHours) * time.Hour),
- })
-}
-
-func (s *Server) clearSessionCookie(w http.ResponseWriter) {
- http.SetCookie(w, &http.Cookie{
- Name: "session",
- Value: "",
- Path: "/",
- HttpOnly: true,
- Secure: s.cfg.SecureCookies,
- SameSite: http.SameSiteStrictMode,
- MaxAge: -1,
- Expires: time.Unix(0, 0),
- })
-}
-
-// ------------------------------------------------------------------
-// Sets
-// ------------------------------------------------------------------
-
-func (s *Server) handleListSets(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- sets, err := s.mediaSvc.ListSets(r.Context(), userIDFromContext(r))
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, sets)
-}
-
-func (s *Server) handleSetCover(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- setID := pathID(r, "id")
- if setID == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid set id"})
- return
- }
- if err := s.mediaSvc.RegenerateSetCover(r.Context(), setID, userIDFromContext(r)); err != nil {
- if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
- return
- }
- if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
- return
- }
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-func (s *Server) handleUpload(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- setID := pathID(r, "id")
- if setID == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid set id"})
- return
- }
-
- maxBytes := int64(s.cfg.MaxUploadSizeMB) << 20
- r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
- if err := r.ParseMultipartForm(maxBytes); err != nil {
- var mbe *http.MaxBytesError
- if errors.As(err, &mbe) {
- writeJSON(w, http.StatusRequestEntityTooLarge, map[string]string{"error": "file too large"})
- return
- }
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid multipart form"})
- return
- }
-
- file, fh, err := r.FormFile("file")
- if err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "missing file"})
- return
- }
- defer file.Close()
-
- media, err := s.mediaSvc.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"})
- return
- }
- if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
- return
- }
- if errors.Is(err, service.ErrUnsupportedExtension) {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, media)
-}
-
-// ------------------------------------------------------------------
-// Media
-// ------------------------------------------------------------------
-
-// parseMediaListQuery extracts and validates query parameters from the request
-// and returns a populated repository.MediaFilter with sensible defaults.
-func parseMediaListQuery(q url.Values) repository.MediaFilter {
- filter := repository.MediaFilter{
- Search: q.Get("search"),
- Sort: q.Get("sort"),
- Limit: 100,
- Offset: 0,
- }
- if v := q.Get("set_id"); v != "" {
- if id, err := strconv.ParseInt(v, 10, 64); err == nil {
- filter.SetID = &id
- }
- }
- if v := q.Get("set_ids"); v != "" {
- parts := strings.Split(v, ",")
- for _, p := range parts {
- if id, err := strconv.ParseInt(strings.TrimSpace(p), 10, 64); err == nil {
- filter.SetIDs = append(filter.SetIDs, id)
- }
- }
- }
- if v := q.Get("type"); v != "" {
- t := model.MediaType(v)
- filter.Type = &t
- }
- if v := q.Get("favorites"); v == "true" || v == "1" {
- filter.Favorites = true
- }
- if v := q.Get("tags"); v != "" {
- filter.Tags = strings.Split(v, ",")
- }
- if v := q.Get("min_duration"); v != "" {
- if f, err := strconv.ParseFloat(v, 64); err == nil {
- filter.MinDuration = &f
- }
- }
- if v := q.Get("max_duration"); v != "" {
- if f, err := strconv.ParseFloat(v, 64); err == nil {
- filter.MaxDuration = &f
- }
- }
- if v := q.Get("filesize_min"); v != "" {
- if n, err := strconv.ParseInt(v, 10, 64); err == nil {
- filter.MinFileSize = &n
- }
- }
- if v := q.Get("filesize_max"); v != "" {
- if n, err := strconv.ParseInt(v, 10, 64); err == nil {
- filter.MaxFileSize = &n
- }
- }
- if v := q.Get("limit"); v != "" {
- if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 1000 {
- filter.Limit = n
- }
- }
- if v := q.Get("offset"); v != "" {
- if n, err := strconv.Atoi(v); err == nil && n >= 0 {
- filter.Offset = n
- }
- }
- return filter
-}
-
-func (s *Server) handleListMedia(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- path := r.URL.Path
- q := r.URL.Query()
- setID := q.Get("set_id")
- setIDs := q.Get("set_ids")
- search := q.Get("search")
- typ := q.Get("type")
- start := time.Now()
- filter := parseMediaListQuery(q)
- media, err := s.mediaSvc.ListMedia(r.Context(), userIDFromContext(r), filter)
- dur := time.Since(start)
- if err != nil {
- fmt.Printf("[api] %s set_id=%s set_ids=%s search=%q type=%s error=%v (took %s)\n", path, setID, setIDs, search, typ, err, dur)
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- fmt.Printf("[api] %s set_id=%s set_ids=%s search=%q type=%s returned=%d (took %s)\n", path, setID, setIDs, search, typ, len(media), dur)
- writeJSON(w, http.StatusOK, media)
-}
-
-func (s *Server) handleGetMedia(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
- return
- }
- detail, err := s.mediaSvc.GetMediaDetail(r.Context(), id, userIDFromContext(r))
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- if detail == nil {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
- return
- }
- writeJSON(w, http.StatusOK, detail)
-}
-
-func (s *Server) handleFavorite(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
- return
- }
- fav, err := s.mediaSvc.ToggleFavorite(r.Context(), userIDFromContext(r), id)
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]bool{"favorite": fav})
-}
-
-func (s *Server) handleAddTag(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "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"})
- return
- }
- if err := s.mediaSvc.AssignTag(r.Context(), id, userIDFromContext(r), req.Tag); err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-func (s *Server) handleRemoveTag(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- tagName := r.PathValue("tag")
- if id == 0 || tagName == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid parameters"})
- return
- }
- if err := s.mediaSvc.RemoveTag(r.Context(), id, userIDFromContext(r), tagName); err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-func (s *Server) handleSoftDelete(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
- return
- }
- if err := s.mediaSvc.SoftDeleteMedia(r.Context(), id, userIDFromContext(r)); err != nil {
- if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
- return
- }
- if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
- return
- }
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
- return
- }
- if err := s.mediaSvc.RestoreMedia(r.Context(), id, userIDFromContext(r)); err != nil {
- if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
- return
- }
- if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
- return
- }
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-// ------------------------------------------------------------------
// File serving helpers
// ------------------------------------------------------------------
@@ -582,434 +134,6 @@ func (s *Server) serveFileResult(w http.ResponseWriter, r *http.Request, res *se
http.ServeContent(w, r, res.FileName, stat.ModTime(), f)
}
-func (s *Server) fileHandler(fn func(context.Context, int64, int64) (*service.FileResult, error)) http.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) {
- id := pathID(r, "id")
- if id == 0 {
- http.Error(w, "invalid media id", http.StatusBadRequest)
- return
- }
- res, err := fn(r.Context(), id, userIDFromContext(r))
- if err != nil {
- if errors.Is(err, service.ErrNotFound) {
- http.Error(w, "not found", http.StatusNotFound)
- return
- }
- if errors.Is(err, service.ErrForbidden) {
- http.Error(w, "forbidden", http.StatusForbidden)
- return
- }
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if res == nil {
- http.Error(w, "not found", http.StatusNotFound)
- return
- }
- s.serveFileResult(w, r, res, false)
- }
-}
-
-func (s *Server) handleStream(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- s.fileHandler(s.mediaSvc.StreamMedia)(w, r)
-}
-
-func (s *Server) handleDownload(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
- return
- }
- res, err := s.mediaSvc.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"})
- return
- }
- if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "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"})
- return
- }
- s.serveFileResult(w, r, res, true)
-}
-
-func (s *Server) handleThumbnail(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- s.fileHandler(s.mediaSvc.GetThumbnail)(w, r)
-}
-
-func (s *Server) handleRegenThumbnail(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
- return
- }
- if err := s.mediaSvc.RegenerateThumbnail(r.Context(), id, userIDFromContext(r)); err != nil {
- if errors.Is(err, service.ErrNotFound) {
- writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
- return
- }
- if errors.Is(err, service.ErrForbidden) {
- writeJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
- return
- }
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-// ------------------------------------------------------------------
-// Share routes
-// ------------------------------------------------------------------
-
-func (s *Server) handleCreateShare(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
- return
- }
- expiresAt := time.Now().Add(time.Duration(s.cfg.ShareDefaultExpiryDays) * 24 * time.Hour)
- share, err := s.mediaSvc.CreateShare(r.Context(), userIDFromContext(r), id, expiresAt)
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, share)
-}
-
-func (s *Server) handleListShares(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
- return
- }
- shares, err := s.mediaSvc.ListShares(r.Context(), id, userIDFromContext(r))
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, shares)
-}
-
-func (s *Server) handleRevokeShare(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- token := r.PathValue("token")
- if token == "" {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "token required"})
- return
- }
- if err := s.mediaSvc.RevokeShare(r.Context(), token, userIDFromContext(r)); err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-func (s *Server) handleSharePage(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- token := r.PathValue("token")
- share, err := s.mediaSvc.ValidateShareToken(r.Context(), token)
- if err != nil || share == nil {
- if err != nil && errors.Is(err, service.ErrShareExpired) {
- http.Error(w, "gone", http.StatusGone)
- return
- }
- http.Error(w, "not found", http.StatusNotFound)
- return
- }
-
- w.Header().Set("Cache-Control", "no-store")
- w.Header().Set("Vary", "Accept")
-
- accept := r.Header.Get("Accept")
- if strings.Contains(accept, "text/html") || accept == "" {
- s.serveFile(w, r, "share.html")
- return
- }
- writeJSON(w, http.StatusOK, share)
-}
-
-func (s *Server) handleShareStream(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- token := r.PathValue("token")
- res, err := s.mediaSvc.StreamSharedMedia(r.Context(), token)
- if err != nil {
- if errors.Is(err, service.ErrShareExpired) {
- http.Error(w, "gone", http.StatusGone)
- return
- }
- if errors.Is(err, service.ErrShareNotFound) || errors.Is(err, service.ErrMediaNotFound) {
- http.Error(w, "not found", http.StatusNotFound)
- return
- }
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if res == nil {
- http.Error(w, "not found", http.StatusNotFound)
- return
- }
- s.serveFileResult(w, r, res, false)
-}
-
-// ------------------------------------------------------------------
-// Notes
-// ------------------------------------------------------------------
-
-func (s *Server) handleGetNote(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
- return
- }
- note, err := s.mediaSvc.GetNote(r.Context(), id, userIDFromContext(r))
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- if note == nil {
- w.WriteHeader(http.StatusNoContent)
- return
- }
- writeJSON(w, http.StatusOK, note)
-}
-
-func (s *Server) handleUpsertNote(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "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"})
- return
- }
- note := &model.Note{MediaID: id, UserID: userIDFromContext(r), Content: req.Content}
- if err := s.mediaSvc.UpsertNote(r.Context(), note); err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, note)
-}
-
-func (s *Server) handleDeleteNote(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.mediaSvc) {
- return
- }
- id := pathID(r, "id")
- if id == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid media id"})
- return
- }
- if err := s.mediaSvc.DeleteNote(r.Context(), id, userIDFromContext(r)); err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-// ------------------------------------------------------------------
-// Progress
-// ------------------------------------------------------------------
-
-func (s *Server) handleProgress(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.progressSvc) {
- return
- }
- var req struct {
- MediaID int64 `json:"media_id"`
- Position float64 `json:"position_seconds"`
- }
- if err := readJSON(r, &req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid body"})
- return
- }
- if req.MediaID == 0 {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "media_id required"})
- return
- }
- err := s.progressSvc.UpdateProgress(
- r.Context(),
- sessionIDFromContext(r),
- userIDFromContext(r),
- req.MediaID,
- req.Position,
- )
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-// ------------------------------------------------------------------
-// Admin
-// ------------------------------------------------------------------
-
-func (s *Server) handleListTrash(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.adminSvc) {
- return
- }
- items, err := s.adminSvc.ListTrash(r.Context())
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, items)
-}
-
-func (s *Server) handleRescan(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.adminSvc) {
- return
- }
- if err := s.adminSvc.TriggerRescan(r.Context()); err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.adminSvc) {
- return
- }
- users, err := s.adminSvc.ListUsers(r.Context())
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, users)
-}
-
-func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.adminSvc) {
- return
- }
- var req struct {
- Username string `json:"username"`
- Password string `json:"password"`
- 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"})
- return
- }
- user, err := s.adminSvc.CreateUser(r.Context(), req.Username, req.Password, req.IsAdmin)
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, user)
-}
-
-func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.adminSvc) {
- return
- }
- 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"})
- return
- }
- if err := s.adminSvc.DeleteUser(r.Context(), id); err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-func (s *Server) handleListPermissions(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.adminSvc) {
- return
- }
- perms, err := s.adminSvc.ListPermissions(r.Context())
- if err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, perms)
-}
-
-func (s *Server) handleGrantPermission(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.adminSvc) {
- return
- }
- var req struct {
- SetID int64 `json:"set_id"`
- UserID int64 `json:"user_id"`
- Role model.Role `json:"role"`
- }
- if err := readJSON(r, &req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
- return
- }
- if err := s.adminSvc.GrantPermission(r.Context(), req.SetID, req.UserID, req.Role); err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
-func (s *Server) handleRevokePermission(w http.ResponseWriter, r *http.Request) {
- if !requireService(w, s.adminSvc) {
- return
- }
- var req struct {
- SetID int64 `json:"set_id"`
- UserID int64 `json:"user_id"`
- }
- if err := readJSON(r, &req); err != nil {
- writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
- return
- }
- if err := s.adminSvc.RevokePermission(r.Context(), req.SetID, req.UserID); err != nil {
- writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
- return
- }
- writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
-}
-
// mimeTypeForFilename returns an HTTP Content-Type based on the file extension.
func mimeTypeForFilename(name string) string {
ext := strings.ToLower(filepath.Ext(name))
diff --git a/internal/api/handlers_admin.go b/internal/api/handlers_admin.go
new file mode 100644
index 0000000..246a466
--- /dev/null
+++ b/internal/api/handlers_admin.go
@@ -0,0 +1,135 @@
+package api
+
+import (
+ "net/http"
+
+ "codeberg.org/snonux/player/internal/model"
+)
+
+// ------------------------------------------------------------------
+// Admin
+// ------------------------------------------------------------------
+
+func (s *Server) handleListTrash(w http.ResponseWriter, r *http.Request) {
+ if !requireService(w, s.adminSvc) {
+ return
+ }
+ items, err := s.adminSvc.ListTrash(r.Context())
+ if err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, items)
+}
+
+func (s *Server) handleRescan(w http.ResponseWriter, r *http.Request) {
+ if !requireService(w, s.adminSvc) {
+ return
+ }
+ if err := s.adminSvc.TriggerRescan(r.Context()); err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
+}
+
+func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) {
+ if !requireService(w, s.adminSvc) {
+ return
+ }
+ users, err := s.adminSvc.ListUsers(r.Context())
+ if err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, users)
+}
+
+func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
+ if !requireService(w, s.adminSvc) {
+ return
+ }
+ var req struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ 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"})
+ return
+ }
+ user, err := s.adminSvc.CreateUser(r.Context(), req.Username, req.Password, req.IsAdmin)
+ if err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, user)
+}
+
+func (s *Server) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
+ if !requireService(w, s.adminSvc) {
+ return
+ }
+ 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"})
+ return
+ }
+ if err := s.adminSvc.DeleteUser(r.Context(), id); err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
+}
+
+func (s *Server) handleListPermissions(w http.ResponseWriter, r *http.Request) {
+ if !requireService(w, s.adminSvc) {
+ return
+ }
+ perms, err := s.adminSvc.ListPermissions(r.Context())
+ if err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, perms)
+}
+
+func (s *Server) handleGrantPermission(w http.ResponseWriter, r *http.Request) {
+ if !requireService(w, s.adminSvc) {
+ return
+ }
+ var req struct {
+ SetID int64 `json:"set_id"`
+ UserID int64 `json:"user_id"`
+ Role model.Role `json:"role"`
+ }
+ if err := readJSON(r, &req); err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
+ return
+ }
+ if err := s.adminSvc.GrantPermission(r.Context(), req.SetID, req.UserID, req.Role); err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
+}
+
+func (s *Server) handleRevokePermission(w http.ResponseWriter, r *http.Request) {
+ if !requireService(w, s.adminSvc) {
+ return
+ }
+ var req struct {
+ SetID int64 `json:"set_id"`
+ UserID int64 `json:"user_id"`
+ }
+ if err := readJSON(r, &req); err != nil {
+ writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
+ return
+ }
+ if err := s.adminSvc.RevokePermission(r.Context(), req.SetID, req.UserID); err != nil {
+ writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ return
+ }
+ writeJSON(w, http.StatusOK, map[string]string{"status":