From 7c1689ca7fc9705045a3f5ecbd0a3e8292dd7e71 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 13 Sep 2026 04:41:04 +0200 Subject: [PATCH] fix: Editorial is behind the Editorial door, including its article pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two editorial article routes were in the authenticated block rather than the moderator one, so any signed-in learner could open /editorial/articles/331 — the read view, but under Editorial's name and with a crumb to a queue they are refused. They sit behind the same guard as the queue now. And the way through from Reading is offered to moderators and admins only. It was offered on `canEdit`, which also covers whoever wrote the article — not the same thing, and for a plain author that link is a Forbidden page with a friendly name on it. An ?edit=1 on a reading address follows the same rule: a moderator is taken to the editorial address, and for anybody else the parameter is simply dropped. Sending a learner to a door they cannot open is worse than ignoring a query string they did not type. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN --- frontend/src/App.jsx | 15 +++++++++------ frontend/src/pages/ArticlesPage.jsx | 17 ++++++++++++----- frontend/src/pages/ArticlesPage.test.jsx | 5 +++-- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 67079ee..1a3e44e 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -235,12 +235,6 @@ function AppRoutes() { {/* Cross-references in article prose address a topic by slug, which outlives a numeric id and is what an educator actually writes. */} } /> - {/* The same page at Editorial's own address. An article reached - from the queue is being worked on, not read, and the trail out - of it should go back to the queue — which it cannot do if the - two are the same URL. */} - } /> - } /> } /> {/* One front door. The dashboard's contents are sections of Settings now; the old address still works for anyone who @@ -265,6 +259,15 @@ function AppRoutes() { } /> } /> } /> + {/* The same page as Reading, at Editorial's own address: an + article reached from the queue is being worked on, not read, + and the trail out of it goes back to the queue. Behind the + same door as the queue itself — it started out in the + authenticated block, where any signed-in learner could open + the editorial view of an article and find a crumb to a page + they are not allowed on. */} + } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/ArticlesPage.jsx b/frontend/src/pages/ArticlesPage.jsx index 7032934..201c081 100644 --- a/frontend/src/pages/ArticlesPage.jsx +++ b/frontend/src/pages/ArticlesPage.jsx @@ -314,10 +314,13 @@ export function ArticlePage() { // taken to the editorial one rather than ignored: the ask is legitimate, the // address is the wrong one. useEffect(() => { - if (!inEditorial && searchParams.get('edit') === '1' && id) { - navigate(`/editorial/articles/${id}?edit=1`, { replace: true }) - } - }, [inEditorial, searchParams, id, navigate]) + if (inEditorial || searchParams.get('edit') !== '1' || !id) return + // Only for somebody who may go there. For anybody else the parameter is + // just dropped: sending a learner to a door they cannot open is worse + // than ignoring a query string they did not type. + if (user?.is_moderator) navigate(`/editorial/articles/${id}?edit=1`, { replace: true }) + else navigate(`/articles/${id}`, { replace: true }) + }, [inEditorial, searchParams, id, navigate, user?.is_moderator]) // Reading claims the window; editing hands it back. Width only — the // navbar's own section strip is left alone, because every link on it is @@ -477,7 +480,11 @@ export function ArticlePage() { // is how somebody ended up in edit mode on Bronchiolitis in the middle of a // session. The same page at /editorial/articles/:id has all of it. const editorActions = !inEditorial ? ( - canEdit ? ( + // Moderators and admins, and nobody else. `canEdit` also covers whoever + // wrote the article, which is not the same thing: Editorial is behind the + // moderator door, so offering that door to an author who is a plain user + // is offering them a Forbidden page. + user?.is_moderator ? ( Open in Editorial diff --git a/frontend/src/pages/ArticlesPage.test.jsx b/frontend/src/pages/ArticlesPage.test.jsx index 936354f..302e019 100644 --- a/frontend/src/pages/ArticlesPage.test.jsx +++ b/frontend/src/pages/ArticlesPage.test.jsx @@ -307,8 +307,9 @@ describe('topic reading', () => { expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull() expect(screen.queryByRole('button', { name: /Unpublish|Publish/ })).toBeNull() expect(screen.queryByRole('button', { name: /Generate cards/ })).toBeNull() - // And a learner is not offered the other mode either — the door belongs to - // whoever may edit. + // And a learner is not offered the other mode either. Editorial is behind + // the moderator door, so the way through belongs to whoever may go there — + // not to whoever happens to have written the article. expect(screen.queryByRole('link', { name: 'Open in Editorial' })).toBeNull() })