fix(media): gate file serving by library visibility; proper download URL
ServeFile previously authenticated only ("any logged-in user") and never
checked that the user can actually see the library owning the file, so
knowing a library UUID + path was enough to fetch content from hidden
libraries. Library visibility is the permission model - the library is
what grants access to its media.
- ServeFile now resolves two URL forms through one flow:
/uploads/library-{id}/{path} (covers, reader files)
/api/media-items/{id}/download (explicit book download, new)
The item form looks up the media item, derives its library and file
path, and adds a Content-Disposition attachment header.
- Both forms enforce GetUserVisibleLibraries for the authenticated
user, mirroring the OPDS download handler (403 when not visible).
- Deleted the dead MediaHandler.DownloadBook handler (never routed).
Also widen media_highlights.start_position/end_position from
VARCHAR(100) to TEXT: the API handlers validate up to 1000 characters
(full Readium locators, KOReader CRE xpointers) but the column rejected
anything longer at the database layer. Metadata-only change applied
idempotently at startup; existing rows are untouched.
Verified against the running server: download 200 + attachment headers
+ epub bytes, unauthenticated 401, user hidden from the library 403 on
both URL forms, visible user 200, covers unchanged, and a 334-char
locator JSON now round-trips through the highlights API.
This commit is contained in:
@@ -335,8 +335,8 @@ CREATE TABLE IF NOT EXISTS media_highlights (
|
||||
media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
selection_text TEXT NOT NULL,
|
||||
start_position VARCHAR(100), -- position (page:offset or CFI) where highlight starts
|
||||
end_position VARCHAR(100), -- position (page:offset or CFI) where highlight ends
|
||||
start_position TEXT, -- position (page:offset, CFI, or locator JSON) where highlight starts
|
||||
end_position TEXT, -- position (page:offset, CFI, or locator JSON) where highlight ends
|
||||
color VARCHAR(7) DEFAULT '#ffff00', -- hex color code for highlight
|
||||
note_id UUID REFERENCES media_notes(id) ON DELETE SET NULL, -- optional associated note
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||
@@ -1364,6 +1364,13 @@ ALTER TABLE media_highlights ADD COLUMN IF NOT EXISTS note_text TEXT;
|
||||
ALTER TABLE media_highlights ADD COLUMN IF NOT EXISTS deleted BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE media_highlights ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||
|
||||
-- Widen position columns for existing databases: the API handlers
|
||||
-- validate up to 1000 characters (full Readium locators, KOReader CRE
|
||||
-- xpointers) but VARCHAR(100) rejected anything longer at the database
|
||||
-- layer. VARCHAR -> TEXT is a metadata-only change, safe to re-run.
|
||||
ALTER TABLE media_highlights ALTER COLUMN start_position TYPE TEXT;
|
||||
ALTER TABLE media_highlights ALTER COLUMN end_position TYPE TEXT;
|
||||
|
||||
ALTER TABLE media_notes ADD COLUMN IF NOT EXISTS dedup_key VARCHAR(40);
|
||||
ALTER TABLE media_notes ADD COLUMN IF NOT EXISTS last_modified_at TIMESTAMPTZ;
|
||||
ALTER TABLE media_notes ADD COLUMN IF NOT EXISTS last_modified_source VARCHAR(30);
|
||||
|
||||
Reference in New Issue
Block a user