fix: visibility-aware feeds — owner sees all, followers see followers-only, home feed includes non-public posts
Some checks failed
lint / lint (push) Has been cancelled
test / unit (push) Has been cancelled
test / integration (push) Has been cancelled
lint / lint (pull_request) Failing after 9m44s
test / unit (pull_request) Successful in 16m12s
test / integration (pull_request) Failing after 16m59s

This commit is contained in:
2026-05-14 18:18:42 +02:00
parent fcbd132a78
commit a5ea97bbaa

View File

@@ -142,7 +142,7 @@ impl FeedRepository for PgFeedRepository {
let ids: Vec<uuid::Uuid> = following_ids.iter().map(|id| id.as_uuid()).collect();
let viewer = viewer_id.map(|v| v.as_uuid());
let total: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM thoughts t WHERE t.user_id=ANY($1) AND t.visibility='public'",
"SELECT COUNT(*) FROM thoughts t WHERE t.user_id=ANY($1) AND t.visibility != 'direct'",
)
.bind(&ids)
.fetch_one(&self.pool)
@@ -150,7 +150,7 @@ impl FeedRepository for PgFeedRepository {
.map_err(|e| DomainError::Internal(e.to_string()))?;
let sel = feed_select(viewer);
let sql = format!("{sel} WHERE t.user_id=ANY($1) AND t.visibility='public' ORDER BY t.created_at DESC LIMIT $2 OFFSET $3");
let sql = format!("{sel} WHERE t.user_id=ANY($1) AND t.visibility != 'direct' ORDER BY t.created_at DESC LIMIT $2 OFFSET $3");
let rows = sqlx::query_as::<_, FeedRow>(&sql)
.bind(&ids)
.bind(page.limit())
@@ -281,20 +281,25 @@ impl FeedRepository for PgFeedRepository {
let viewer = viewer_id.map(|v| v.as_uuid());
let uid = user_id.as_uuid();
// Use nil UUID for unauthenticated viewers — won't match owner or follower checks.
let viewer_uuid = viewer.unwrap_or(uuid::Uuid::nil());
let total: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM thoughts t WHERE t.user_id = $1 AND t.visibility = 'public'",
"SELECT COUNT(*) FROM thoughts t WHERE t.user_id = $1 AND ($2::uuid = $1 OR (t.visibility != 'direct' AND (t.visibility IN ('public', 'unlisted') OR (t.visibility = 'followers' AND EXISTS(SELECT 1 FROM follows WHERE follower_id = $2 AND following_id = $1 AND state = 'accepted')))))",
)
.bind(uid)
.bind(viewer_uuid)
.fetch_one(&self.pool)
.await
.map_err(|e| DomainError::Internal(e.to_string()))?;
let sel = feed_select(viewer);
let sql = format!("{sel} WHERE t.user_id = $1 AND t.visibility = 'public' ORDER BY t.created_at DESC LIMIT $2 OFFSET $3");
let sql = format!("{sel} WHERE t.user_id = $1 AND ($4::uuid = $1 OR (t.visibility != 'direct' AND (t.visibility IN ('public', 'unlisted') OR (t.visibility = 'followers' AND EXISTS(SELECT 1 FROM follows WHERE follower_id = $4 AND following_id = $1 AND state = 'accepted'))))) ORDER BY t.created_at DESC LIMIT $2 OFFSET $3");
let rows = sqlx::query_as::<_, FeedRow>(&sql)
.bind(uid)
.bind(page.limit())
.bind(page.offset())
.bind(viewer_uuid)
.fetch_all(&self.pool)
.await
.map_err(|e| DomainError::Internal(e.to_string()))?;