refactor(vis): H4.5 steps 4-5 — statistics and the three reader gates on the canonical resolver; core goes package-level

VisibleLibrarySet becomes a package-level function taking the queries
(db-only services like statistics reach it without new wiring); the
LibraryService method stays as the ergonomic entry point. Statistics'
ids-shaper and the reader pages' three hand-rolled membership loops
(empty-rows fallback then linear scans) now delegate; the gates keep
their 403-on-invisible semantics (>null set membership, never silent
pass).

Census addendum from this pass: two OPDS sites call
GetUserVisibleLibraries directly but consume FULL ROWS (per-library
folder enumeration) — a third legitimate pattern alongside set/ids and
resolveLibrary's active-selection; documented, untouched. The library
LIST endpoint and its SSR data helper are row consumers too, by design.
This commit is contained in:
John O'Keefe committed 2026-10-17 17:35:43 -04:00
1 parent 8dbaa8f810
commit 454938c31f
3 files changed
+27 -39

No files matched your search

+14 -25
View File
@@ -78,19 +78,14 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
}
userData := user.(database.Users)
// Get user's visible libraries
libraries, err := h.libraryService.GetUserVisibleLibraries(c.Request().Context(), userData.ID)
// Family-lens gate (H4.5 canonical resolver): the media item's
// library must be in the requester's visible set — 403, never a
// silent pass.
visibleSet, _, err := h.libraryService.VisibleLibrarySet(c.Request().Context(), userData.ID)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"})
}
// Check if media item's library is in visible list
visible := false
for _, lib := range libraries {
if lib.ID == mediaItem.LibraryID {
visible = true
break
}
}
visible := visibleSet[uuid.UUID(mediaItem.LibraryID.Bytes).String()]
if !visible {
return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"})
}
@@ -172,19 +167,14 @@ func (h *ReaderHandler) GetPage(c *echo.Context) error {
}
userData := user.(database.Users)
// Get user's visible libraries
libraries, err := h.libraryService.GetUserVisibleLibraries(c.Request().Context(), userData.ID)
// Family-lens gate (H4.5 canonical resolver): the media item's
// library must be in the requester's visible set — 403, never a
// silent pass.
visibleSet, _, err := h.libraryService.VisibleLibrarySet(c.Request().Context(), userData.ID)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"})
}
// Check if media item's library is in visible list
visible := false
for _, lib := range libraries {
if lib.ID == mediaItem.LibraryID {
visible = true
break
}
}
visible := visibleSet[uuid.UUID(mediaItem.LibraryID.Bytes).String()]
if !visible {
return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"})
}
@@ -225,14 +215,13 @@ func (h *ReaderHandler) ensureItemVisible(c *echo.Context, mediaItem *database.M
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "User not authenticated"})
}
userData := user.(database.Users)
libraries, err := h.libraryService.GetUserVisibleLibraries(c.Request().Context(), userData.ID)
// Family-lens gate (H4.5 canonical resolver).
visibleSet, _, err := h.libraryService.VisibleLibrarySet(c.Request().Context(), userData.ID)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to check library access"})
}
for _, lib := range libraries {
if lib.ID == mediaItem.LibraryID {
return nil
}
if visibleSet[uuid.UUID(mediaItem.LibraryID.Bytes).String()] {
return nil
}
return c.JSON(http.StatusForbidden, map[string]string{"error": "Access denied to this library"})
}