refactor(vis): H4.5 step 1 — LibraryService.VisibleLibrarySet, the canonical resolver; media endpoints migrated

One core for "which libraries can this user see": family-lens default-
allow stays where it always was (the GetUserVisibleLibraries SQL);
the core adds the shared set+ids shaping that four endpoints each
hand-rolled. Media's visibleLibrarySet is now a thin identity adapter
(JWT user object -> resolved id) and the download-permission gate uses
the set instead of a manual membership loop.

Semantic diff (H4.5 step 1, in the plan): zero family-lens divergence
found across all six sites — the rule lives in one query; the variation
was shape-level (identity adapter, set-vs-ids, UUID idiom).
This commit is contained in:
John O'Keefe committed 2026-10-17 17:30:11 -04:00
1 parent 29a546229d
commit 1d12a64915
2 files changed
+30 -22

No files matched your search

+5 -22
View File
@@ -654,25 +654,14 @@ func (h *MediaHandler) HandleBulkUpdate(c *echo.Context) error {
// visibleLibrarySet loads the requester's visible libraries — membership // visibleLibrarySet loads the requester's visible libraries — membership
// map for param checks, id slice for SQL ANY() filters. // map for param checks, id slice for SQL ANY() filters.
// visibleLibrarySet adapts the media endpoints' identity source (the
// JWT user object) to the canonical resolver (H4.5).
func (mh *MediaHandler) visibleLibrarySet(c *echo.Context) (map[string]bool, []pgtype.UUID, error) { func (mh *MediaHandler) visibleLibrarySet(c *echo.Context) (map[string]bool, []pgtype.UUID, error) {
user, ok := c.Get("user").(database.Users) user, ok := c.Get("user").(database.Users)
if !ok { if !ok {
return nil, nil, echo.NewHTTPError(http.StatusUnauthorized, "invalid user") return nil, nil, echo.NewHTTPError(http.StatusUnauthorized, "invalid user")
} }
libraries, err := mh.libraryService.GetUserVisibleLibraries(c.Request().Context(), user.ID) return mh.libraryService.VisibleLibrarySet(c.Request().Context(), user.ID)
if err != nil {
return nil, nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to check library access")
}
set := make(map[string]bool, len(libraries))
ids := make([]pgtype.UUID, 0, len(libraries))
for _, lib := range libraries {
if !lib.ID.Valid {
continue
}
set[uuid.UUID(lib.ID.Bytes).String()] = true
ids = append(ids, lib.ID)
}
return set, ids, nil
} }
// mediaStorageState reports whether the library storage holding a media // mediaStorageState reports whether the library storage holding a media
@@ -2517,17 +2506,11 @@ func (mh *MediaHandler) ServeFile(c *echo.Context) error {
// Library visibility gate: the library is what grants permission to // Library visibility gate: the library is what grants permission to
// see and download media. // see and download media.
user := c.Get("user").(database.Users) user := c.Get("user").(database.Users)
visibleLibraries, err := mh.libraryService.GetUserVisibleLibraries(c.Request().Context(), user.ID) visibleSet, _, err := mh.libraryService.VisibleLibrarySet(c.Request().Context(), user.ID)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to check library access"}) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to check library access"})
} }
libraryVisible := false libraryVisible := visibleSet[uuid.UUID(libraryUUID.Bytes).String()]
for _, lib := range visibleLibraries {
if lib.ID.Valid && lib.ID.Bytes == libraryUUID.Bytes {
libraryVisible = true
break
}
}
if !libraryVisible { if !libraryVisible {
return c.JSON(http.StatusForbidden, map[string]string{"error": "library not accessible"}) return c.JSON(http.StatusForbidden, map[string]string{"error": "library not accessible"})
} }
+25
View File
@@ -4,6 +4,7 @@ import (
"bookhoard/internal/database" "bookhoard/internal/database"
"context" "context"
"fmt" "fmt"
"github.com/google/uuid"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@@ -174,6 +175,30 @@ func (s *LibraryService) SetLibraryVisibility(ctx context.Context, userID, libra
} }
// GetUserVisibleLibraries retrieves all libraries visible to a user // GetUserVisibleLibraries retrieves all libraries visible to a user
// VisibleLibrarySet is THE canonical library-visibility resolver (H4.5):
// every endpoint that asks "which libraries can this user see" funnels
// here. The family-lens rule itself lives in the
// GetUserVisibleLibraries SQL (COALESCE(lv.is_visible, true) —
// default-allow: an admin hides a library, everyone else still sees it).
// Callers adapt their own identity source (JWT user object, device
// token, SSR session); this core takes the resolved user id.
func (s *LibraryService) VisibleLibrarySet(ctx context.Context, userID pgtype.UUID) (map[string]bool, []pgtype.UUID, error) {
rows, err := s.GetUserVisibleLibraries(ctx, userID)
if err != nil {
return nil, nil, fmt.Errorf("failed to check library access: %w", err)
}
set := make(map[string]bool, len(rows))
ids := make([]pgtype.UUID, 0, len(rows))
for _, lib := range rows {
if !lib.ID.Valid {
continue
}
set[uuid.UUID(lib.ID.Bytes).String()] = true
ids = append(ids, lib.ID)
}
return set, ids, nil
}
func (s *LibraryService) GetUserVisibleLibraries(ctx context.Context, userID pgtype.UUID) ([]database.GetUserVisibleLibrariesRow, error) { func (s *LibraryService) GetUserVisibleLibraries(ctx context.Context, userID pgtype.UUID) ([]database.GetUserVisibleLibrariesRow, error) {
return s.db.GetUserVisibleLibraries(ctx, userID) return s.db.GetUserVisibleLibraries(ctx, userID)
} }