5 Commits

Author SHA1 Message Date
Deluan Quintão
f853ca604a
refactor(db): migrate all ids to a uniform canonical 128-bit base62 encoding (#5824)
* refactor(model): extract canonical 128-bit base62 id codec

* feat(model): generate random ids as canonical 128-bit base62 values

* feat(scanner): emit legacy PIDs in canonical base62 encoding

* feat(db): add id canonicalization transform for the uniform-ids migration

* feat(db): migrate all ids to canonical 128-bit base62 encoding

* fix(db): canonicalize ids in junction tables and JSON columns

* chore(jellyfin): update id-family notes for uniform canonical ids

* test(ids): harden codec input contract and migration edge coverage

* refactor(model): use log.Fatal for Encode128 contract guard per project convention

* fix(db): force full rescan after id migration for legacy PID configs

* test(db): guard id-column inventory against schema drift

* refactor(ids): compile-time Encode128 contract and unified column rewrite helper

* refactor(db): apply review feedback to id migration

Filter empty strings in collectColumn's SQL, reuse a prepared statement
for rewriteColumn updates, and clarify the legacy ID functions' comment
now that they emit the canonical encoding.

* feat(auth): split session and public-link JWT secrets, rotating sessions on id migration

* test(subsonic): initialize public token secret in helpers suite

The suite sets auth.TokenAuth directly instead of calling auth.Init, so the
new PublicTokenAuth was nil whenever Ginkgo's spec order ran a helpers spec
before any spec that calls auth.Init, panicking in publicurl.ImageURL.

* refactor(db): inline canonicalID into its only consumer, the uniform-ids migration

* refactor(model): rename Encode128/Decode128 to Encode/Decode

With every id now exactly 128 bits, the width suffix is redundant; the
package-qualified id.Encode/id.Decode carries the same information.

* test(db): make the id-columns guard classify JSON columns too

The guard only inspected columns named id/pid/*_id, so it could not see ids
embedded in JSON. Widen it to *_ids and to every JSON column, and drive the
"covered" set from a new embeddedIDColumns list instead of the inline calls
in the migration.

Every JSON column the schema has now carries a verdict. The four denormalized
caches -- media_file/album.participants, media_file/album.tags,
album.folder_ids and artist.similar_artists -- hold only artist, tag and
folder ids. Those all come from id.NewHash, whose 22-char base62 encoding of
a 128-bit MD5 is already in canonical range, so canonicalID is the identity
on them and the migration correctly leaves them alone. A new codec test pins
that invariant, since the exemptions depend on it.

Verified on a copy of a 727MB/96k-track production database: canonicalizing
those four columns changed zero rows, and artist, tag and folder ids were
themselves unchanged by the migration (only media_file ids moved, 95108 of
96666).
2026-08-02 12:58:53 -04:00
Deluan Quintão
969e7e108c
fix(share): enforce track membership on public share streams (#5769)
* fix(share): enforce track membership on public share streams

The public share stream endpoint (GET /share/s/{jwt}) validated that the
share existed, was unexpired, and that the share owner had library access
to the requested track, but it never verified that the track was actually
a member of the share. It also accepted stream tokens with no share id
(sid) claim, skipping share checks entirely.

Enforce that the requested media file belongs to share.Tracks, and make
the sid claim mandatory on the stream path. The only producer of stream
tokens (encodeMediafileShare) always sets sid, so no legitimate flow is
affected; the image endpoint decodes independently and is unchanged.

Also document why a JWT is used to represent a shared track: it is a
signed, scoped capability for a single public share, not part of
authentication.

* docs(share): clarify JWT usage comment wording
2026-07-13 09:04:24 -04:00
Deluan Quintão
fa138afea5
fix(playlist/share): apply user library access to import and sharing paths (#5640)
* fix(playlist): respect the user's library access when resolving M3U paths

FindByPaths looked up paths across all libraries, so importing an M3U
could add tracks from libraries the importing user has no access to.
Apply the user's library filter to the lookup, matching every other
media_file read. Admins and the (admin-context) scanner are unaffected.

* fix(share): scope shared playlist tracks to the owner's libraries

loadMedia loaded playlist tracks with a fake-admin context, so a shared
playlist could include tracks from libraries the owner has no access to.
Load as the share owner instead, so the library filter applies.
Admin-owned shares are unchanged.

* fix(share): only serve shared tracks the owner can access

A shared stream fetched the media file by id without checking the share
owner's library access, so it could serve tracks from libraries the
owner has no access to. Gate share-scoped streams on the owner's library
access. Non-share streams are unaffected.

* test(share): tidy library-access test setup

Consolidate the repeated share-owner test fixture in handleStream into a
helper, assert on track fields with HaveField instead of building an id
slice, and delete the scratch media file through the public repository
method.

* style: trim verbose comments in library-access checks

* fix(share): guard against nil owner and clean up test users

Add a nil check after loading the share owner so a missing user yields a
clear error instead of a possible nil dereference, and delete the users
created by the new tests in their AfterEach blocks.

* fix(share): avoid panic when a shared playlist is no longer visible to its owner

Tracks() returns nil when the playlist can't be loaded under the owner's
context (e.g. a public playlist shared by a non-owner that was later made
private). Capture the result and return early instead of chaining GetAll
on a nil repository, leaving the share with no tracks.
2026-06-23 18:53:51 -04:00
Deluan
efe9291db0 refactor: multiple syntax updates for Go 1.26
Signed-off-by: Deluan <deluan@navidrome.org>
2026-05-19 18:02:36 -03:00
Deluan Quintão
5c4f0298a6
fix(sharing): validate JWT expiration and share existence on stream endpoint (#5426)
* fix(sharing): validate JWT expiration and share existence on stream endpoint

The public stream endpoint (/public/s/{token}) was using
TokenAuth.Decode() which only verifies the JWT signature but skips
exp claim validation. This allowed expired share stream URLs to remain
functional indefinitely. Additionally, deleting a share did not revoke
previously issued stream tokens since the handler never performed a
server-side share lookup.

Fixed by switching decodeStreamInfo() to use auth.Validate() which
properly checks the exp claim, and by embedding the share ID ("sid")
in stream tokens so the handler can verify the share still exists.
Old tokens without the sid claim remain backward compatible but still
benefit from expiration validation.

* fix(sharing): check share expiration on stream requests

Replace the lightweight Exists() check with Get() + expiration
validation, so that shares whose ExpiresAt was updated to an earlier
time after token issuance are also rejected (410 Gone). Reuses the
existing checkShareError handler for consistent error responses.
2026-04-27 19:36:57 -04:00