Skip to content

Commit b3a34d9

Browse files
feat(api): let invited collaborators see private tracks they're on
A collaborator (pending or accepted) couldn't see an unlisted track they were invited to outside the single-track endpoint (which sets IncludeUnlisted), so a private collaboration never showed on their profile/feeds. - get_tracks.sql: an unlisted track is also visible to a user who is an active (pending/accepted, not rejected) collaborator on it. Scoped to collaborator_user_id = @my_id, so it never leaks to other viewers. The EXISTS only evaluates for unlisted tracks (the OR short-circuits public ones) and only for the already-bounded @ids set, indexed by the track_collaborators PK. - v1_users_tracks.go: surface a user's own unlisted accepted collaborations on their own profile (guarded by @my_id = @user_id); hidden from everyone else. Tested: collaborator sees the private track, non-collaborator and rejected collaborator do not. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c9d95c7 commit b3a34d9

4 files changed

Lines changed: 62 additions & 2 deletions

File tree

api/dbv1/get_tracks.sql.go

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/dbv1/queries/get_tracks.sql

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,11 @@ FROM tracks t
226226
JOIN aggregate_track using (track_id)
227227
LEFT JOIN aggregate_plays on play_item_id = t.track_id
228228
LEFT JOIN track_routes on t.track_id = track_routes.track_id and track_routes.is_current = true
229-
WHERE (is_unlisted = false OR t.owner_id = @my_id OR @include_unlisted::bool = TRUE)
229+
WHERE (is_unlisted = false OR t.owner_id = @my_id OR @include_unlisted::bool = TRUE
230+
OR EXISTS (SELECT 1 FROM track_collaborators tc
231+
WHERE tc.track_id = t.track_id
232+
AND tc.collaborator_user_id = @my_id
233+
AND tc.status IN ('pending', 'accepted')))
230234
AND t.track_id = ANY(@ids::int[])
231235
AND (t.access_authorities IS NULL
232236
OR (COALESCE(@authed_wallet, '') <> ''

api/v1_track_collaborators_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,49 @@ func TestTrackCollaboratorNotificationsGenerated(t *testing.T) {
128128
assert.NoError(t, err)
129129
assert.Equal(t, 1, acceptCount, "accepted credit should notify the inviter (user 500)")
130130
}
131+
132+
// An invited collaborator can see a private (unlisted) track they're on; other
133+
// users cannot. Exercises the get_tracks visibility clause directly.
134+
func TestCollaboratorSeesPrivateTrack(t *testing.T) {
135+
app := testAppWithFixtures(t)
136+
ctx := context.Background()
137+
138+
// Track 700 (owned by user 500) is private/unlisted.
139+
_, err := app.pool.Replicas[0].Exec(ctx,
140+
"UPDATE tracks SET is_unlisted = true WHERE track_id = 700 AND is_current = true")
141+
assert.NoError(t, err)
142+
143+
// User 1 is a pending collaborator (hasn't accepted yet) — they still need
144+
// to see the track to decide.
145+
now := time.Now()
146+
database.SeedTable(app.pool.Replicas[0], "track_collaborators", []map[string]any{
147+
{"track_id": 700, "collaborator_user_id": 1, "invited_by": 500, "status": "pending", "created_at": now, "updated_at": now},
148+
})
149+
150+
// Collaborator (user 1) sees the private track.
151+
rows, err := app.queries.GetTracks(ctx, dbv1.GetTracksParams{
152+
Ids: []int32{700},
153+
MyID: int32(1),
154+
})
155+
assert.NoError(t, err)
156+
assert.Len(t, rows, 1, "an invited collaborator should see the private track")
157+
158+
// A non-collaborator (user 2) does not.
159+
rows, err = app.queries.GetTracks(ctx, dbv1.GetTracksParams{
160+
Ids: []int32{700},
161+
MyID: int32(2),
162+
})
163+
assert.NoError(t, err)
164+
assert.Len(t, rows, 0, "a non-collaborator must not see the private track")
165+
166+
// Once the collaborator declines, they no longer see it.
167+
_, err = app.pool.Replicas[0].Exec(ctx,
168+
"UPDATE track_collaborators SET status = 'rejected' WHERE track_id = 700 AND collaborator_user_id = 1")
169+
assert.NoError(t, err)
170+
rows, err = app.queries.GetTracks(ctx, dbv1.GetTracksParams{
171+
Ids: []int32{700},
172+
MyID: int32(1),
173+
})
174+
assert.NoError(t, err)
175+
assert.Len(t, rows, 0, "a rejected collaborator must not see the private track")
176+
}

api/v1_users_tracks.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ func (app *ApiServer) v1UserTracks(c *fiber.Ctx) error {
9292
// another owner for collab tracks, so the pin references the profile user.
9393
ownerFilter = "(t.owner_id = @user_id OR t.track_id = ANY(@collab_track_ids))"
9494
pinExpr = "t.track_id = (SELECT artist_pick_track_id FROM users WHERE user_id = @user_id)"
95+
// Surface the user's own unlisted collaborations on their own profile
96+
// (my_id == user_id); a private collab track stays hidden from other
97+
// viewers, who only see it once it's public.
98+
if params.FilterTracks != "public" {
99+
trackFilter = "(" + trackFilter + " OR (t.track_id = ANY(@collab_track_ids) AND @my_id = @user_id))"
100+
}
95101
}
96102

97103
// The profile lists a user's own tracks plus tracks they've accepted a

0 commit comments

Comments
 (0)