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()
})