summaryrefslogtreecommitdiff
path: root/internal/api
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-30 11:28:21 +0300
committerPaul Buetow <paul@buetow.org>2026-04-30 11:28:21 +0300
commit100a9bb282dd4226368b4e5fac398726c7acf653 (patch)
tree61730d7bc0d10cddd674ba24438dad2a381b88d1 /internal/api
parentadecf1fd2e55a0605d03f36952846f6336506d37 (diff)
task da: enforce media access and owner/admin role permissions
Changes: - MediaService.ListMedia now accepts userID and filters by allowed sets for non-admins via AllowedSetIDs in repository.MediaFilter. - Handlers pass userID into ListMedia; API returns 403 for forbidden. - Added verifyModifyAccess and verifySetModifyAccess helpers so only owners/admins can upload, soft-delete, restore, and regenerate thumbnails/covers; viewers are blocked. - GetMediaDetail, ToggleFavorite, AssignTag, RemoveTag, notes, and shares now consistently verifyAccess before proceeding. - Handlers handle ErrForbidden with 403 for soft-delete and restore. - Added negative tests proving viewers cannot mutate and unauthorized users cannot access/detail/tag/note/favorite/share inaccessible media.
Diffstat (limited to 'internal/api')
-rw-r--r--internal/api/handlers.go18
-rw-r--r--internal/api/handlers_test.go42
2 files changed, 58 insertions, 2 deletions
diff --git a/internal/api/handlers.go b/internal/api/handlers.go
index fc1d43d..af6a0a0 100644
--- a/internal/api/handlers.go
+++ b/internal/api/handlers.go
@@ -356,7 +356,7 @@ func (s *Server) handleListMedia(w http.ResponseWriter, r *http.Request) {
return
}
filter := parseMediaListQuery(r.URL.Query())
- media, err := s.mediaSvc.ListMedia(r.Context(), filter)
+ media, err := s.mediaSvc.ListMedia(r.Context(), userIDFromContext(r), filter)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
@@ -452,6 +452,14 @@ func (s *Server) handleSoftDelete(w http.ResponseWriter, r *http.Request) {
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
}
@@ -468,6 +476,14 @@ func (s *Server) handleRestore(w http.ResponseWriter, r *http.Request) {
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
}
diff --git a/internal/api/handlers_test.go b/internal/api/handlers_test.go
index c853140..798ac81 100644
--- a/internal/api/handlers_test.go
+++ b/internal/api/handlers_test.go
@@ -587,7 +587,7 @@ func TestServer_MediaList(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
var gotFilter repository.MediaFilter
ms := &service.MockMediaService{
- ListMediaFunc: func(ctx context.Context, filter repository.MediaFilter) ([]model.Media, error) {
+ ListMediaFunc: func(ctx context.Context, userID int64, filter repository.MediaFilter) ([]model.Media, error) {
gotFilter = filter
return tt.listResult, tt.listErr
},
@@ -782,6 +782,46 @@ func TestServer_Restore(t *testing.T) {
}
}
+func TestServer_SoftDelete_Forbidden(t *testing.T) {
+ ms := &service.MockMediaService{
+ SoftDeleteMediaFunc: func(ctx context.Context, mediaID, userID int64) error {
+ return service.ErrForbidden
+ },
+ }
+ store := buildSessionStore(1)
+ sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
+ cfg := &internal.Config{SessionTimeoutHours: 24}
+ srv := newTestServer(t, buildCountStore(1), nil, sm, cfg, ms, nil, nil, nil)
+
+ req := httptest.NewRequest(http.MethodDelete, "/api/media/99", nil)
+ req.AddCookie(addSessionCookie(t, store, sm, 1))
+ rr := httptest.NewRecorder()
+ srv.ServeHTTP(rr, req)
+ if rr.Code != http.StatusForbidden {
+ t.Fatalf("expected %d, got %d", http.StatusForbidden, rr.Code)
+ }
+}
+
+func TestServer_Restore_Forbidden(t *testing.T) {
+ ms := &service.MockMediaService{
+ RestoreMediaFunc: func(ctx context.Context, mediaID, userID int64) error {
+ return service.ErrForbidden
+ },
+ }
+ store := buildSessionStore(1)
+ sm := auth.NewSessionManager(store, &clock.MockClock{T: time.Now()}, time.Hour)
+ cfg := &internal.Config{SessionTimeoutHours: 24}
+ srv := newTestServer(t, buildCountStore(1), nil, sm, cfg, ms, nil, nil, nil)
+
+ req := httptest.NewRequest(http.MethodPost, "/api/media/99/restore", nil)
+ req.AddCookie(addSessionCookie(t, store, sm, 1))
+ rr := httptest.NewRecorder()
+ srv.ServeHTTP(rr, req)
+ if rr.Code != http.StatusForbidden {
+ t.Fatalf("expected %d, got %d", http.StatusForbidden, rr.Code)
+ }
+}
+
// ------------------------------------------------------------------
// Notes
// ------------------------------------------------------------------