fix: Editorial is behind the Editorial door, including its article pages
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:
parent
e62a742b73
commit
7c1689ca7f
3 changed files with 24 additions and 13 deletions
|
|
@ -235,12 +235,6 @@ function AppRoutes() {
|
||||||
{/* Cross-references in article prose address a topic by slug, which
|
{/* Cross-references in article prose address a topic by slug, which
|
||||||
outlives a numeric id and is what an educator actually writes. */}
|
outlives a numeric id and is what an educator actually writes. */}
|
||||||
<Route path="/articles/s/:slug" element={<ArticlePage />} />
|
<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 />} />
|
<Route path="/flashcards/:deckId/study" element={<FlashcardStudyPage />} />
|
||||||
{/* One front door. The dashboard's contents are sections of
|
{/* One front door. The dashboard's contents are sections of
|
||||||
Settings now; the old address still works for anyone who
|
Settings now; the old address still works for anyone who
|
||||||
|
|
@ -265,6 +259,15 @@ function AppRoutes() {
|
||||||
<Route path="/trash" element={<TrashPage />} />
|
<Route path="/trash" element={<TrashPage />} />
|
||||||
<Route path="/categories" element={<CategoriesPage />} />
|
<Route path="/categories" element={<CategoriesPage />} />
|
||||||
<Route path="/editorial" element={<EditorialPage />} />
|
<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="/access" element={<AccessPage />} />
|
||||||
<Route path="/questions/new" element={<QuestionEditPage mode="create" />} />
|
<Route path="/questions/new" element={<QuestionEditPage mode="create" />} />
|
||||||
<Route path="/questions/:id" element={<QuestionEditPage />} />
|
<Route path="/questions/:id" element={<QuestionEditPage />} />
|
||||||
|
|
|
||||||
|
|
@ -314,10 +314,13 @@ export function ArticlePage() {
|
||||||
// taken to the editorial one rather than ignored: the ask is legitimate, the
|
// taken to the editorial one rather than ignored: the ask is legitimate, the
|
||||||
// address is the wrong one.
|
// address is the wrong one.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!inEditorial && searchParams.get('edit') === '1' && id) {
|
if (inEditorial || searchParams.get('edit') !== '1' || !id) return
|
||||||
navigate(`/editorial/articles/${id}?edit=1`, { replace: true })
|
// 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
|
||||||
}, [inEditorial, searchParams, id, navigate])
|
// 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
|
// 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
|
// 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
|
// 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.
|
// session. The same page at /editorial/articles/:id has all of it.
|
||||||
const editorActions = !inEditorial ? (
|
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">
|
<Link to={`/editorial/articles/${article.id}`} className="btn btn-secondary btn-sm">
|
||||||
Open in Editorial
|
Open in Editorial
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
||||||
|
|
@ -307,8 +307,9 @@ describe('topic reading', () => {
|
||||||
expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull()
|
expect(screen.queryByRole('button', { name: 'Edit' })).toBeNull()
|
||||||
expect(screen.queryByRole('button', { name: /Unpublish|Publish/ })).toBeNull()
|
expect(screen.queryByRole('button', { name: /Unpublish|Publish/ })).toBeNull()
|
||||||
expect(screen.queryByRole('button', { name: /Generate cards/ })).toBeNull()
|
expect(screen.queryByRole('button', { name: /Generate cards/ })).toBeNull()
|
||||||
// And a learner is not offered the other mode either — the door belongs to
|
// And a learner is not offered the other mode either. Editorial is behind
|
||||||
// whoever may edit.
|
// 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()
|
expect(screen.queryByRole('link', { name: 'Open in Editorial' })).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue