Skip to content

fix(admin): restrict admin-only routes to administrators - #3020

Open
jmirchandani wants to merge 4 commits into
emdash-cms:mainfrom
jmirchandani:fix/settings-admin-gate
Open

fix(admin): restrict admin-only routes to administrators#3020
jmirchandani wants to merge 4 commits into
emdash-cms:mainfrom
jmirchandani:fix/settings-admin-gate

Conversation

@jmirchandani

@jmirchandani jmirchandani commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Non-admin users could reach admin-only screens in the admin UI. The sidebar and command palette already hide these entries below Admin, but the user menu showed a Settings link to every role, and the admin-only routes mounted for anyone who typed their URL. There, requests the server rejects left screens that failed to load or could not save.

This PR:

  • adds a RequireAdmin route guard, the same in-component pattern the byline schema page already uses: a loader while the current user loads, then either the page or an "Access denied" screen;
  • wraps the /settings hub and its site-level pages (General, Social Links, SEO, Self-Signup Domains, API Tokens, Email, Backups), /users, /import/wordpress and /plugins-manager/$pluginId/settings in that guard;
  • shows the user-menu Settings link only to admins.

Deliberately left open: /settings/security (each user's own passkeys) and /settings/media-usage (it already has its own admin gate). /content-types, /plugins-manager, /plugins/marketplace and /themes/marketplace stay reachable for Editors because the server allows their reads at Editor.

Behaviour change for Editors, also stated in the changeset: Editors can no longer open Settings → General, Social Links and SEO. The server allows reading those at settings:read, but saving already required Admin. Non-admins also lose the language switcher that lives on the Settings hub; they can still choose a language on the sign-in screen.

There is no existing issue; this was found while auditing role access across the admin UI.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main.
  • I have added and reviewed the user-facing changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...
  • I have included screenshots below if this PR changes the UI

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Code with Claude Fable 5.1, Claude Sonnet 5 and Claude Opus 5

Screenshots / test output

Screenshots: captured with Playwright from demos/simple running this branch (b873df21), signed in as the dev user, first with the Admin role and then with the Editor role.

Admin, user menu: "Security Settings" and "Settings" (unchanged).

Admin user menu showing Security Settings and Settings

Editor, user menu: "Security Settings" only; the "Settings" link is gone.

Editor user menu showing only Security Settings and Log out

Editor, opening /_emdash/admin/settings by URL: "Access denied" with "You need admin permissions to view this page." instead of the page.

Editor sees Access denied on the Settings page

Editor, opening /_emdash/admin/settings/security: unchanged, still works.

Editor can still open Security Settings

The "Plugin error (403)" card on the Editor dashboard comes from the demo's audit history plugin widget; this PR does not touch plugins.

Tests (packages/admin): npx vitest run router-admin-gate RequireAdmin Header tests/components/settings/ → 13 test files, 136 tests passed. router-admin-gate.test.tsx mounts every wrapped route as an Editor and asserts the "Access denied" screen (and the page itself for an Admin), with a mirror table asserting the four Editor-reachable routes stay reachable; removing any wrap makes its case fail.

Checks: pnpm build, pnpm typecheck and pnpm lint pass. Formatting was checked on the changed files with both oxfmt and prettier; a repo-wide pnpm format:check on this Windows checkout flags nearly every file because of CRLF line endings, which is unrelated to this change.


🤖 Generated with Claude Code

https://claude.ai/code/session_01AznEK6RvKGboBkFg4XqadT

The user menu showed a Settings link to every role, and admin-only
routes mounted for anyone who typed their URL. The sidebar and command
palette already hide these entries below Admin, but that is navigation,
not a guard: a non-admin could still open the /settings hub and its
site-level pages, /users, /import/wordpress and a plugin's settings
page, where requests the server rejects left screens that failed to
load or could not save.

Add a RequireAdmin route guard, using the same in-component pattern the
byline schema page already uses, around those routes, and show the user
menu's Settings link only to admins. /settings/security stays open to
every role because it manages the signed-in user's own passkeys.

Editors lose read access to General, Social Links and SEO, which the
server allows at settings:read, and non-admins lose the language
switcher on the Settings hub. Both are deliberate and described in the
changeset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AznEK6RvKGboBkFg4XqadT
@changeset-bot

changeset-bot Bot commented Sep 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fd94ab0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 17 packages
Name Type
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
create-emdash Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/release-service Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

Copy link
Copy Markdown
Contributor

Scope check

This PR changes 599 lines across 7 files. Large PRs are harder to review and more likely to be closed without review.

If this scope is intentional, no action needed. A maintainer will review it. If not, please consider splitting this into smaller PRs.

See CONTRIBUTING.md for contribution guidelines.

@pkg-pr-new

pkg-pr-new Bot commented Sep 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@3020

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@3020

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@3020

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@3020

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@3020

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@3020

emdash

npm i https://pkg.pr.new/emdash@3020

create-emdash

npm i https://pkg.pr.new/create-emdash@3020

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@3020

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@3020

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@3020

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@3020

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@3020

@emdash-cms/registry-moderation

npm i https://pkg.pr.new/@emdash-cms/registry-moderation@3020

@emdash-cms/registry-verification

npm i https://pkg.pr.new/@emdash-cms/registry-verification@3020

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@3020

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@3020

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@3020

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@3020

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@3020

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@3020

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@3020

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@3020

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@3020

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@3020

commit: fd94ab0

@jmirchandani
jmirchandani marked this pull request as ready for review September 10, 2026 16:06
@github-actions github-actions Bot added the review/needs-review No maintainer or bot review yet label Sep 10, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the right fix for the right problem. Non-admin users could indeed land on admin-only UI routes by typing URLs, and the in-component RequireAdmin guard matches the existing BylineSchemaPage pattern while respecting the server’s actual RBAC tiers: it gates the settings hub and its admin-only sub-pages, /users, /import/wordpress, and auto-generated plugin settings, but deliberately leaves /content-types, /plugins-manager, and the marketplaces unwrapped because the server already allows Editor reads there. I confirmed against packages/auth/src/rbac.ts, packages/core/src/astro/routes/api/settings.ts, the plugin settings endpoint, Sidebar.tsx, MediaUsageSettings.tsx, and Settings.tsx that the role checks and the language-switcher claim line up. Tests are behavior-oriented and provide good regression coverage.

The only real issues are large-scale violations of the repo’s comment-discipline conventions. Several new files and route definitions carry essay-length comments that justify the change, reference internal cards (emdb-023), cite specific line numbers in other files, and narrate rejected alternatives. Per AGENTS.md, comments are for future readers, not reviewers; justification, cross-references, and issue/PR numbers should live in the PR description and commit message. These should be pruned before merge. The code underneath is sound.

What I checked:

  • Full diff and changed files (RequireAdmin.tsx, Header.tsx, router.tsx, tests, changeset)
  • Auth RBAC and the relevant server-side permission checks
  • Sidebar.tsx nav gating for consistency
  • MediaUsageSettings.tsx own admin gate
  • Settings.tsx language switcher location
  • Changeset against .changeset/README.md — the changeset is user-facing and accurate, no issue found

Comment thread packages/admin/src/router.tsx Outdated
Comment thread packages/admin/src/components/RequireAdmin.tsx Outdated
Comment thread packages/admin/src/components/Header.tsx Outdated
Comment thread packages/admin/tests/router-admin-gate.test.tsx Outdated
jmirchandani and others added 2 commits September 10, 2026 14:03
Trim review-narrative comments from the admin gate change down to
short factual ones per house style, and shorten two describe() titles
in router-admin-gate.test.tsx. No executable code changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AznEK6RvKGboBkFg4XqadT
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review size/L and removed review/needs-review No maintainer or bot review yet size/XL labels Sep 10, 2026
# Conflicts:
#	packages/admin/tests/components/Header.test.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/admin review/needs-rereview Author pushed changes since the last review size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant