diff --git a/crates/ipx/CHANGELOG.md b/crates/ipx/CHANGELOG.md index b6bef30..4fd964e 100644 --- a/crates/ipx/CHANGELOG.md +++ b/crates/ipx/CHANGELOG.md @@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `ipx user passwd` signs the account out of every browser, and says how many of its API tokens still work. A session signed in with the old password went on working (#334). - Live updates about a feed go only to the people who read it, and admins. Everyone signed in had every feed's, errors included, which carried a paid feed's address with its key (#325). - An open page stops getting live updates within half a minute of its account being removed. They went on until the page was closed (#335). +- Only someone subscribed to a feed, or an admin, may mark it private. Anyone signed in could keep anyone's feed, or a whole OPML's, out of the directory, and only an admin could undo it (#333). ## [1.4.0] - 2026-10-19 diff --git a/crates/ipx/src/web.rs b/crates/ipx/src/web.rs index 31d6a62..8c487b7 100644 --- a/crates/ipx/src/web.rs +++ b/crates/ipx/src/web.rs @@ -1822,6 +1822,11 @@ async fn patch_feed( "the feed's address, folder, schedule and category, and whether it may be shared, are the same for everyone, so only an admin changes them", )); } + // Marking a feed private is for those who read it: anyone could keep anyone's feed, or a + // whole OPML's, out of the directory, and only an admin could undo it (#333). + if !user.is_admin && state.ctx.db.subscription(user.id, &id).instrument(step("subscription")).await?.is_none() { + return Err(ApiError::not_found("you do not subscribe to that feed")); + } let mut cfg = state.ctx.edit_cfg().await; // Derived feeds have no config entry. Editing one is the moment it earns a real diff --git a/tests/ui/app.spec.js b/tests/ui/app.spec.js index 2039afa..2754b11 100644 --- a/tests/ui/app.spec.js +++ b/tests/ui/app.spec.js @@ -694,6 +694,8 @@ test('a second person has their own feeds and their own read state', async ({ br // Hiding the way in is not the guard; the server is. expect((await page.request.get('/api/users')).status()).toBe(403); expect((await page.request.get('/api/logs')).status()).toBe(403); + // Nor may Sam mark a feed they do not read private, keeping it out of the directory (#333). + expect((await page.request.patch('/api/feeds/test-show', { data: { private: true } })).status()).toBe(404); // Subscribing to a feed the admin already has costs no second fetch: same feed, same // files, but Sam's own read state.