diff --git a/internal/handlers/media.go b/internal/handlers/media.go index a8812387..a37a04a8 100644 --- a/internal/handlers/media.go +++ b/internal/handlers/media.go @@ -654,25 +654,14 @@ func (h *MediaHandler) HandleBulkUpdate(c *echo.Context) error { // visibleLibrarySet loads the requester's visible libraries — membership // 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) { user, ok := c.Get("user").(database.Users) if !ok { return nil, nil, echo.NewHTTPError(http.StatusUnauthorized, "invalid user") } - libraries, err := mh.libraryService.GetUserVisibleLibraries(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 + return mh.libraryService.VisibleLibrarySet(c.Request().Context(), user.ID) } // 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 // see and download media. 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 { return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to check library access"}) } - libraryVisible := false - for _, lib := range visibleLibraries { - if lib.ID.Valid && lib.ID.Bytes == libraryUUID.Bytes { - libraryVisible = true - break - } - } + libraryVisible := visibleSet[uuid.UUID(libraryUUID.Bytes).String()] if !libraryVisible { return c.JSON(http.StatusForbidden, map[string]string{"error": "library not accessible"}) } diff --git a/internal/services/library_service.go b/internal/services/library_service.go index b7b91691..1fb93799 100644 --- a/internal/services/library_service.go +++ b/internal/services/library_service.go @@ -4,6 +4,7 @@ import ( "bookhoard/internal/database" "context" "fmt" + "github.com/google/uuid" "log" "os" "path/filepath" @@ -174,6 +175,30 @@ func (s *LibraryService) SetLibraryVisibility(ctx context.Context, userID, libra } // 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) { return s.db.GetUserVisibleLibraries(ctx, userID) }