Skip to content

Add admin user plan updates across API, MCP, and admin UI - #682

Merged
kentcdodds merged 3 commits into
mainfrom
cursor/admin-user-record-updates-1110
Jul 8, 2026
Merged

kentcdodds merged 3 commits into
mainfrom
cursor/admin-user-record-updates-1110

Conversation

@kentcdodds

@kentcdodds kentcdodds commented Jul 8, 2026 •

Copy link
Copy Markdown
Member

What

Admins can now modify a user account record's entitlement plan — previously the only way to set users.plan was a manual SQL update.

  • Users admin API — POST /admin/users.json gains an update_plan action (guarded by update:user:any) that sets or clears (plan: null → legacy/unlimited) a user's plan. Unknown plan strings are rejected with 400 rather than coerced to null. Success writes an admin-category audit event with reason target_user_id=…;plan=….
  • MCP — new admin_user_update capability (admin domain, requiredRole: 'admin', audited via auditAdminCapabilityInvocation) updates one user by id or email with plan: PlanName | null and returns the updated account metadata record.
  • Admin UI — /admin/users shows the plan in the metadata grid and adds a "Manage plan" panel with a plan select (including "Legacy/unlimited (no plan)") and a Save button that stays disabled while the selection matches the stored plan.
  • Payload shape — the admin users API/MCP metadata now includes plan (validated through parsePlanName, so unknown stored values read back as null) and availablePlans; the privacy-boundary field pin and its tests were widened accordingly.

Username editing was deliberately left out: usernames drive {username}@<platform domain> email routing and public identity lookups, so an admin rename has side effects beyond account metadata.

Testing

  • Unit: update_plan handler actions (set / clear / invalid plan 400 / unknown user 404 / audit reasons), admin_user_update MCP capability (set/clear by email/id, audit rows, non-admin rejection, unknown-plan rejection), and parsePlanName parsing.
  • E2E: admin-rbac.spec.ts now changes a plan through the UI, asserts the success message, and verifies plan via the API alongside the widened payload key pin.
  • Manual GUI test on the local dev server (video below): set jane's plan to pro, then cleared it back to legacy/unlimited; audit log recorded update_plan events with the expected reasons.
  • npm run validate passes (format, lint, typecheck, unit, Playwright E2E, MCP E2E).

admin_users_plan_editing_full_flow.mp4

Admin users page with jane

Docs

  • docs/contributing/architecture/authorization.md — POST actions, privacy boundary (plan is account metadata), widened field pin.
  • docs/contributing/architecture/entitlements.md — new "Assigning plans" section covering both audited admin surfaces.
  • docs/contributing/adding-capabilities.md — admin_user_update added to the admin capability list.
System recap — extends existing primitives (medium risk)

Mode: recap · Base: main @ 80e35c57 · Head: b1b453f0

Classification: extends — the RBAC admin surface gains a plan mutation
(HTTP action + MCP capability) and the admin users payload contract widens to
include plan/availablePlans. No primitives added; primitives.yaml
unchanged.

Primitives touched

Primitive Group Impact
rbac auth extends — update_plan action on POST /admin/users.json, new admin_user_update MCP capability, payload pin widened with plan
entitlements auth composes — reuses planNames/parsePlanName for validation; plan config and enforcement unchanged
app-ui surfaces extends — /admin/users gains a Manage plan panel and Plan metadata field
mcp-server surfaces composes — one more capability registered in the existing admin domain

System map

flowchart LR
	appUi["app-ui"]:::extended
	mcpServer["mcp-server"]:::touched
	rbac["rbac"]:::extended
	entitlements["entitlements"]:::touched
	d1AppDb["d1-app-db"]:::untouched
	capabilityRegistry["capability-registry"]:::untouched
	appUi --> rbac
	mcpServer --> capabilityRegistry --> rbac
	rbac --> d1AppDb
	rbac --> entitlements
	classDef touched fill:#1a7f37,color:#fff
	classDef extended fill:#9a6700,color:#fff
	classDef added fill:#cf222e,color:#fff
	classDef untouched fill:#57606a,color:#fff
Loading

Change flow

sequenceDiagram
	participant Admin
	participant UI as /admin/users
	participant API as POST /admin/users.json
	participant MCP as admin_user_update
	participant D1 as users.plan
	Admin->>UI: select plan, Save plan
	UI->>API: { action: update_plan, userId, plan | null }
	Admin->>MCP: { id | email, plan | null }
	API->>D1: updateAdminUserPlan (parsePlanName-validated)
	MCP->>D1: updateAdminUserPlan (zod planNames-validated)
	API-->>Admin: refreshed users payload + audit event
	MCP-->>Admin: updated user metadata + audit event
Loading

Before / after

Surface Before After
users.plan writes manual SQL only audited admin UI action + admin_user_update MCP
Admin users payload fields id…roles (no plan) + plan (parsed), + availablePlans
Admin MCP domain list/get/create + update (plan set/clear)

Invariants

per-user-isolation: the new mutation stays inside the documented RBAC
exception — admin-only guards (update:user:any / requiredRole: 'admin'),
account metadata only, audit-logged; the privacy-boundary field pin test was
widened deliberately to include plan and still rejects any other widening.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Admin users can now view and update account plans.
    • The admin users screen now includes a plan selector and shows each user’s current plan.
    • The admin tools now support setting or clearing a plan, including legacy/unlimited accounts.
  • Bug Fixes

    • Admin user lists now include plan and email-verification details consistently.
    • Plan changes are now reflected immediately in refreshed admin user data.

- users admin API: new update_plan action on POST /admin/users.json
  (update:user:any), validating against the plan registry and audit
  logging target_user_id and plan
- new admin_user_update MCP capability (admin role, audited) to set or
  clear a user's entitlement plan by id or email
- admin users payload and MCP metadata schema now expose plan; the
  /admin/users page gains a Manage plan panel and Plan metadata field
- tests: handler actions, MCP capability, parsePlanName, and the
  admin RBAC E2E now covers plan editing and the widened payload pin
- docs: authorization privacy boundary and entitlements plan
  assignment
@coderabbitai

coderabbitai Bot commented Jul 8, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cursor[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 11 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9e73216a-c0af-46bd-9c3d-998c37365c3f

📥 Commits

Reviewing files that changed from the base of the PR and between 34e000f and f4a46a1.

📒 Files selected for processing (2)
  • docs/contributing/architecture/entitlements.md
  • packages/worker/src/entitlements/entitlements.node.test.ts
📝 Walkthrough

Walkthrough

This PR adds entitlement "plan" support to admin user management: users.plan is exposed through admin data loaders and API responses, a new update_plan action is added to the admin-users API handler, a new admin_user_update MCP capability is introduced, an admin UI panel is added to manage plans, and docs/tests are updated accordingly.

Changes

Admin plan assignment feature

Layer / File(s) Summary
Plan data model and storage
packages/worker/src/app/admin-users-data.ts, packages/worker/src/app/loader-data.ts, packages/worker/src/entitlements/entitlements.node.test.ts
Adds plan column selection/mapping to admin user queries, extends AdminUserListItem/loadAdminUsersData with plan/availablePlans, and renames AdminUsagePlanName to AdminPlanName; adds parsePlanName tests.
Admin users API update_plan action
packages/worker/src/app/handlers/admin-users.ts, packages/worker/src/app/handlers/admin-users.node.test.ts
Adds update_plan POST action with validation via readPlanUpdate, persistence via updateAdminUserPlan, audit logging, and 400/404 error handling, with full test coverage.
admin_user_update MCP capability
packages/worker/src/mcp/capabilities/admin/admin-user-update.ts, admin-shared.ts, domain.ts, admin-capabilities.node.test.ts
Adds planNameSchema/plan field to shared schema, creates the admin_user_update capability with input validation and audited plan updates, registers it in adminDomain, and adds tests.
Admin UI plan management panel
packages/worker/client/routes/admin-users.tsx, packages/worker/client/navigation-data.node.test.ts
Extends AdminUsersRoute state/handlers with plan draft tracking and submitPlanAction, adds a "Manage plan" select/save panel and metadata display.
Documentation and e2e validation
docs/contributing/adding-capabilities.md, docs/contributing/architecture/authorization.md, docs/contributing/architecture/entitlements.md, e2e/admin-rbac.spec.ts
Documents admin_user_update, the update_plan action, and privacy boundary changes; extends e2e test to exercise plan selection and verification.

Estimated code review effort: 3 (Moderate) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Admin
  participant AdminUsersRoute
  participant AdminUsersHandler
  participant AdminUsersData
  participant AuditLog

  Admin->>AdminUsersRoute: select plan, click Save plan
  AdminUsersRoute->>AdminUsersHandler: POST action=update_plan
  AdminUsersHandler->>AdminUsersHandler: readPlanUpdate(body.plan)
  AdminUsersHandler->>AdminUsersData: updateAdminUserPlan(userId, plan)
  AdminUsersData-->>AdminUsersHandler: updated user or null
  AdminUsersHandler->>AuditLog: logAuditEvent(update_plan)
  AdminUsersHandler-->>AdminUsersRoute: refreshed users payload
  AdminUsersRoute-->>Admin: show updated plan
Loading

Possibly related PRs

  • kentcdodds/kody#596: The main PR extends the same admin-users RBAC admin UI/API introduced in #596 by adding the plan field and a new update_plan flow (including admin_user_update MCP capability) alongside existing role assignment.
  • kentcdodds/kody#619: The main PR's new admin "plan" management is directly tied to the retrieved PR's core per-user plan/entitlement system that defines PlanName semantics and users.plan behavior for enforcement.
  • kentcdodds/kody#627: Both PRs extend the admin MCP domain by modifying packages/worker/src/mcp/capabilities/admin/domain.ts's capabilities list.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding admin user plan updates across the API, MCP, and admin UI.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/admin-user-record-updates-1110

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kentcdodds
kentcdodds marked this pull request as ready for review July 8, 2026 15:56
@github-actions

github-actions Bot commented Jul 8, 2026 •

Copy link
Copy Markdown
Contributor

🔎 Preview deployed: https://kody-pr-682.kody-a99.workers.dev

Worker: kody-pr-682
D1: kody-pr-682-db
KV: kody-pr-682-oauth-kv

Mocks:

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b1b453f. Configure here.

Comment thread packages/worker/client/routes/admin-users.tsx
Bugbot flagged that an unsaved plan selection survived pagination,
route preloads, and role mutations, so Save plan could apply a stale
draft. Any refresh of the users list now drops the draft and the
render pass reseeds the select from the refreshed record.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
packages/worker/src/app/admin-users-data.ts (1)

59-67: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared SELECT column list.

The id, username, email, email_verified_at, plan, created_at, updated_at column list is duplicated across the list query, the by-id query, and the by-email query. This PR already needed three manual edits to add plan; a future column addition/removal is one easy edit away from silently drifting across these queries.

♻️ Proposed refactor
+const adminUserRowColumns =
+	'id, username, email, email_verified_at, plan, created_at, updated_at'
+
 export async function loadAdminUsersData(
 	env: Env,
 	requestUrl: string,
 ): Promise<AdminUsersLoaderData> {
 	...
 		env.APP_DB.prepare(
-			`SELECT id, username, email, email_verified_at, plan, created_at, updated_at
-			 FROM users
-			 ORDER BY id ASC
-			 LIMIT ? OFFSET ?`,
+			`SELECT ${adminUserRowColumns}
+			 FROM users
+			 ORDER BY id ASC
+			 LIMIT ? OFFSET ?`,
 		)

Apply similarly to the two queries in loadAdminUserByIdOrEmail.

Also applies to: 91-109

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/admin-users-data.ts` around lines 59 - 67, The admin
user queries in admin-users-data.ts repeat the same SELECT column list in the
paginated list query and the lookup queries, which makes future schema changes
easy to drift. Extract that shared column list into a single reusable constant
or helper near loadAdminUsers/loadAdminUserByIdOrEmail, then update all three
queries to reference it so changes to columns like plan only need one edit.
packages/worker/client/routes/admin-users.tsx (1)

168-281: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated fetch/error-handling between submitRoleAction and submitPlanAction.

Both functions repeat the same query-string preservation, fetch options, 401 redirect, JSON parsing, and users/availableRoles/availablePlans/pagination sync logic. Consider extracting a shared helper that performs the POST + response handling, parameterized by request body and success message, to avoid the two copies drifting apart as more admin actions are added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/client/routes/admin-users.tsx` around lines 168 - 281,
`submitRoleAction` and `submitPlanAction` duplicate the same POST/query-string
preservation, 401 redirect, JSON parsing, and state-sync flow, so factor that
work into a shared helper. Extract the common fetch-and-handle logic from these
methods in `admin-users.tsx`, parameterizing the request body and the
success/message update, then have both `submitRoleAction` and `submitPlanAction`
call it so pagination, users, roles, and plans stay in sync in one place.
packages/worker/src/app/loader-data.ts (1)

37-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Export AdminPlanName from #worker/entitlements/plans.ts

AdminPlanName duplicates PlanName; re-exporting keeps the loader types aligned with the shared plan source.

♻️ Proposed refactor
-export type AdminPlanName = 'partner' | 'personal' | 'pro'
+export type { PlanName as AdminPlanName } from '`#worker/entitlements/plans.ts`'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/loader-data.ts` at line 37, The loader type is
depending on a duplicated AdminPlanName instead of the shared plan type source.
Update the plans module referenced by AdminPlanName so it re-exports the shared
PlanName from `#worker/entitlements/plans.ts`, then ensure loader-data’s plan
field continues to use AdminPlanName without redefining a separate type. Keep
the change centered around the AdminPlanName and PlanName symbols so all
plan-related types stay aligned.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/worker/src/app/handlers/admin-users.ts`:
- Around line 302-311: The fire-and-forget audit event logging calls in the
assign_role, remove_role, and update_plan action handlers are not guaranteed to
complete before the response is returned, risking dropped audit writes. Either
await the logAuditEvent calls directly to ensure they complete before
responding, or if async execution is preferred, pass the promise to
ctx.waitUntil to keep the request alive until the audit write completes. Apply
this fix to all three action handlers.

---

Nitpick comments:
In `@packages/worker/client/routes/admin-users.tsx`:
- Around line 168-281: `submitRoleAction` and `submitPlanAction` duplicate the
same POST/query-string preservation, 401 redirect, JSON parsing, and state-sync
flow, so factor that work into a shared helper. Extract the common
fetch-and-handle logic from these methods in `admin-users.tsx`, parameterizing
the request body and the success/message update, then have both
`submitRoleAction` and `submitPlanAction` call it so pagination, users, roles,
and plans stay in sync in one place.

In `@packages/worker/src/app/admin-users-data.ts`:
- Around line 59-67: The admin user queries in admin-users-data.ts repeat the
same SELECT column list in the paginated list query and the lookup queries,
which makes future schema changes easy to drift. Extract that shared column list
into a single reusable constant or helper near
loadAdminUsers/loadAdminUserByIdOrEmail, then update all three queries to
reference it so changes to columns like plan only need one edit.

In `@packages/worker/src/app/loader-data.ts`:
- Line 37: The loader type is depending on a duplicated AdminPlanName instead of
the shared plan type source. Update the plans module referenced by AdminPlanName
so it re-exports the shared PlanName from `#worker/entitlements/plans.ts`, then
ensure loader-data’s plan field continues to use AdminPlanName without
redefining a separate type. Keep the change centered around the AdminPlanName
and PlanName symbols so all plan-related types stay aligned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 892fbf96-9664-43f5-b7d2-ea3c998d5ec8

📥 Commits

Reviewing files that changed from the base of the PR and between 632585f and 34e000f.

📒 Files selected for processing (15)
  • docs/contributing/adding-capabilities.md
  • docs/contributing/architecture/authorization.md
  • docs/contributing/architecture/entitlements.md
  • e2e/admin-rbac.spec.ts
  • packages/worker/client/navigation-data.node.test.ts
  • packages/worker/client/routes/admin-users.tsx
  • packages/worker/src/app/admin-users-data.ts
  • packages/worker/src/app/handlers/admin-users.node.test.ts
  • packages/worker/src/app/handlers/admin-users.ts
  • packages/worker/src/app/loader-data.ts
  • packages/worker/src/entitlements/entitlements.node.test.ts
  • packages/worker/src/mcp/capabilities/admin/admin-capabilities.node.test.ts
  • packages/worker/src/mcp/capabilities/admin/admin-shared.ts
  • packages/worker/src/mcp/capabilities/admin/admin-user-update.ts
  • packages/worker/src/mcp/capabilities/admin/domain.ts

Comment on lines +302 to +311
const requestIp = getRequestIp(input.request) ?? undefined
void logAuditEvent({
category: 'admin',
action: 'update_plan',
result: 'success',
email: input.actor.email,
ip: requestIp,
path: input.url.pathname,
reason: `target_user_id=${targetUserId};plan=${planUpdate.plan ?? 'null'}`,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "logAuditEvent" packages/worker/src/app/handlers/admin-users.ts
rg -n "waitUntil" packages/worker/src --type=ts

Repository: kentcdodds/kody

Length of output: 5184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== admin-users.ts relevant sections =="
sed -n '150,340p' packages/worker/src/app/handlers/admin-users.ts

echo
echo "== request pipeline waitUntil usage in worker src =="
rg -n "waitUntil\\(" packages/worker/src --type=ts

echo
echo "== handler entrypoints near admin-users pipeline =="
rg -n "handleUpdatePlanAction|handleAssignRole|handleRemoveRole|create.*Handler|ctx\\.waitUntil|waitUntil\\(" packages/worker/src/app packages/worker/src/index.ts --type=ts

Repository: kentcdodds/kody

Length of output: 39701


Keep admin audit writes alive. assign_role, remove_role, and update_plan all fire-and-forget logAuditEvent(...), but this request path never uses ctx.waitUntil, so the audit write can be dropped once the response returns. If these events need to be durable, await the call or hand it to ctx.waitUntil.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/worker/src/app/handlers/admin-users.ts` around lines 302 - 311, The
fire-and-forget audit event logging calls in the assign_role, remove_role, and
update_plan action handlers are not guaranteed to complete before the response
is returned, risking dropped audit writes. Either await the logAuditEvent calls
directly to ensure they complete before responding, or if async execution is
preferred, pass the promise to ctx.waitUntil to keep the request alive until the
audit write completes. Apply this fix to all three action handlers.

@kentcdodds
kentcdodds merged commit 1011ace into main Jul 8, 2026
5 checks passed
@kentcdodds
kentcdodds deleted the cursor/admin-user-record-updates-1110 branch July 8, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants