fix: scope book progress and shelves to the requesting user

This commit is contained in:
2026-08-11 20:08:26 -04:00
parent dfeb4c3267
commit a1f39a8dc8
+29 -23
View File
@@ -14,7 +14,7 @@ from advanced_alchemy.extensions.litestar.providers import (
from advanced_alchemy.exceptions import NotFoundError from advanced_alchemy.exceptions import NotFoundError
from advanced_alchemy.filters import CollectionFilter, StatementFilter from advanced_alchemy.filters import CollectionFilter, StatementFilter
from advanced_alchemy.service import FilterTypeT from advanced_alchemy.service import FilterTypeT
from sqlalchemy.orm import selectinload, with_loader_criteria from sqlalchemy.orm import selectinload
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from litestar import Request from litestar import Request
from litestar.params import Dependency, Parameter from litestar.params import Dependency, Parameter
@@ -51,8 +51,19 @@ from chitai.services.filters.book import (
async def provide_book_service( async def provide_book_service(
db_session: AsyncSession, current_user: m.User | None = None db_session: AsyncSession, current_user: m.User = Dependency(skip_validation=True)
) -> AsyncGenerator[BookService, None]: ) -> AsyncGenerator[BookService, None]:
"""
Provide a BookService with per-user data scoped to the caller.
`current_user` is a required dependency, not an optional argument. It used to
default to None with the scoping below wrapped in `if current_user:` — and
when it was not injected, that block was silently skipped, so
`Book.progress_records` and `Book.list_links` loaded *every* user's rows.
`Book.progress` returns `progress_records[0]`, so one user could see another
user's reading position; the shelf checkboxes leaked the same way. Failing
loudly on a missing user is the point of the change.
"""
load = [ load = [
selectinload(m.Book.author_links).selectinload(m.BookAuthorLink.author), selectinload(m.Book.author_links).selectinload(m.BookAuthorLink.author),
selectinload(m.Book.tag_links).selectinload(m.BookTagLink.tag), selectinload(m.Book.tag_links).selectinload(m.BookTagLink.tag),
@@ -60,30 +71,25 @@ async def provide_book_service(
m.Book.files, m.Book.files,
m.Book.identifiers, m.Book.identifiers,
m.Book.series, m.Book.series,
] # Reading progress, restricted to the caller.
#
# Load in specific user-book data # The restriction lives on the relationship via .and_() rather than in a
if current_user: # separate with_loader_criteria(). advanced_alchemy's
# Load progress data # get_abstract_loader_options() keeps only _AbstractLoad,
load.extend( # InstrumentedAttribute, RelationshipProperty and "*" entries and drops
[ # everything else — and with_loader_criteria() is none of those, so the
selectinload(m.Book.progress_records), # previous criteria were discarded before reaching a query. A
with_loader_criteria( # selectinload() carrying its own .and_() survives that filter.
m.BookProgress, m.BookProgress.user_id == current_user.id selectinload(
m.Book.progress_records.and_(m.BookProgress.user_id == current_user.id)
), ),
] # Bookshelf membership, restricted to the caller
selectinload(
m.Book.list_links.and_(
m.BookListLink.book_list.has(m.BookList.user_id == current_user.id)
) )
).selectinload(m.BookListLink.book_list),
# Load shelf data
load.extend(
[
selectinload(m.Book.list_links).selectinload(m.BookListLink.book_list),
with_loader_criteria(
m.BookListLink,
m.Book.lists.any(m.BookList.user_id == current_user.id),
),
] ]
)
provider_func = create_service_provider( provider_func = create_service_provider(
BookService, BookService,