fix: Editorial is behind the Editorial door, including its article pages
Some checks failed
Tests / backend (push) Failing after 3s
Tests / frontend (push) Successful in 25s
Tests / e2e (push) Failing after 30s

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
Daniel 2026-09-13 04:41:04 +02:00
parent e62a742b73
commit 7c1689ca7f
3 changed files with 24 additions and 13 deletions

View file

@ -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. */}
<Route path="/articles/s/:slug" element={<ArticlePage />} />
{/* 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. */}
<Route path="/editorial/articles/:id" element={<ArticlePage />} />
<Route path="/editorial/articles/s/:slug" element={<ArticlePage />} />
<Route path="/flashcards/:deckId/study" element={<FlashcardStudyPage />} />
{/* 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() {
<Route path="/trash" element={<TrashPage />} />
<Route path="/categories" element={<CategoriesPage />} />
<Route path="/editorial" element={<EditorialPage />} />
{/* 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. */}
<Route path="/editorial/articles/:id" element={<ArticlePage />} />
<Route path="/editorial/articles/s/:slug" element={<ArticlePage />} />
<Route path="/access" element={<AccessPage />} />
<Route path="/questions/new" element={<QuestionEditPage mode="create" />} />
<Route path="/questions/:id" element={<QuestionEditPage />} />

View file

@ -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 ? (
<Link to={`/editorial/articles/${article.id}`} className="btn btn-secondary btn-sm">
Open in Editorial
</Link>

View file

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