feat(admin): deployment-admin dashboard (instance operator surface)#4437
Open
viktormarinho wants to merge 9 commits into
Open
feat(admin): deployment-admin dashboard (instance operator surface)#4437viktormarinho wants to merge 9 commits into
viktormarinho wants to merge 9 commits into
Conversation
Instance-level operator dashboard gated by DEPLOYMENT_ADMIN_EMAILS (not a per-org role): list/search users, impersonate, list orgs with member counts, add a member to any org. UI at /_admin with an impersonation indicator in the account popover. Security posture (from the PR review): - Fence the raw Better Auth admin plugin (/api/auth/admin/*) so a pushed adminUserIds id can't reach set-role/set-user-password directly — everything goes through the curated /api/_admin/* routes (auth.api.* in-process). Only stop-impersonating is allowed through. - Mount under /_admin (underscore is illegal in an org slug) so it can never shadow a real org's /:org surface. - Audit impersonate + member-add via PostHog; map better-auth APIError to its real status instead of 500; handle stop-impersonation failure in the UI. Dedup: shared getInitials/formatDate/isAlreadyMemberError helpers and a NoPermissionState action slot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A freshly-impersonated e2e target is emailVerified:false, so requireDeploymentAdmin returns 401 (email_not_verified) before the allowlist 403 branch. Assert the invariant that actually holds — the impersonated session is blocked by the middleware (401 or 403), never allowed through — instead of pinning 403. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
better-call ships in multiple copies, so the APIError thrown by better-auth's plugins isn't identical to the class imported from better-auth/api — the instanceof check missed and addMember-to-a-bad-org 500'd instead of 400. Duck-type on a numeric 4xx/5xx statusCode instead. Fixes the e2e add-member-nonexistent-org assertion (was masked in the prior run by the serial describe stopping at the earlier re-impersonate failure). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
That test was skipped in the prior run (serial describe stopped at the earlier failure), so it never actually exercised the browser sign-in. Set the Origin header on the browser context so the sign-in POST clears Better Auth's CSRF guard, matching newApiContext. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Must-fix: - exempt /api/_admin from SSO enforcement so an admin whose active org enforces SSO isn't locked out of the instance surface; mount + exemption now share one exported ADMIN_API_PREFIX (add endpoints without a 2nd edit) - tighten APIError shape-match to require name==="APIError" so foreign errors carrying a statusCode (e.g. AI SDK) aren't relabeled; +6 unit tests - durable stdout audit line for impersonate/member-add (PostHog-less hosts) - reach the 409 re-impersonation guard in e2e via a 2nd allowlisted admin Recommended: - clamp+search /api/_admin/orgs (ILIKE name/slug) + search box, mirroring /users - two-probe email+name user search so the search placeholder is honest - isError states on both admin pages (was silently "no results") - encapsulate grantDeploymentAdmin(); fence e2e now loops 6 endpoints/methods Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Encapsulating the push behind grantDeploymentAdmin() (previous commit) left deploymentAdminUserIds imported nowhere outside auth/index.ts, so knip failed the build job on an unused export. It's referenced only internally now (grantDeploymentAdmin + the admin-plugin adminUserIds option), so make it module-private rather than silencing knip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…review - Origin check (trustedOrigins) on mutating /api/_admin routes so CSRF safety no longer rides solely on the SameSite cookie default - audit the real actor (impersonatedBy) on member-add; server-side self-impersonation guard - move the /api/auth/admin/* fence into routes/admin.ts (deleting the module now breaks the app.ts import) - project /users to the fields the dashboard renders (drop raw better-auth rows and the branch-dependent total) - surface email_not_verified in the /_admin gate; extract adminFetch - e2e: fence loop covers impersonate-user/update-user, SSO-exemption regression test, granted-admin impersonation pinned both ways, browser test asserts redirect+rows, fail-fast env probe, shared TEST_PASSWORD; document the global APIError mapping + _admin mount-order guarantees Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ymentAdmin A random signup (typically unverified) now gets the plain 403 instead of a misleading 'verify your email' hint — only allowlisted operators see the email_not_verified code the /_admin gate surfaces. Caught by the browser-gate e2e: fresh non-admin users are unverified, so the gate showed the wrong copy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both survived only by accident: the allowlist-before-verification check order had no test that could distinguish it from the reverse, and the /users field projection (which keeps role/banned/banReason off the wire) was only checked for presence of the target row, not its shape.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this contribution about?
Adds an instance-level operator dashboard at
/_admin, gated by theDEPLOYMENT_ADMIN_EMAILSenv allowlist (not a per-org role), for debugging user issues and fixing org membership: search/list users, impersonate a user, list orgs with member counts, and add a member to any org — with an impersonation indicator in the account popover. The raw Better Auth admin plugin (/api/auth/admin/*) is fenced off so a pushedadminUserIdsid can't callset-role/set-user-passworddirectly (everything routes through the curated/api/_admin/*handlers, which callauth.api.*in-process); the surface is mounted under_adminbecause an underscore is illegal in an org slug and can't shadow a real org. Impersonation and member-add are audited via PostHog, thrown better-authAPIErrors are mapped to their real HTTP status instead of 500, and shared helpers (getInitials/formatDate/isAlreadyMemberError, plus aNoPermissionStateaction slot) remove duplicated logic.Screenshots/Demonstration
UI-affecting (admin dashboard + impersonation pill). Screenshots to be added.
How to Test
DEPLOYMENT_ADMIN_EMAILS=you@example.comand sign in as that (verified) user./_adminand confirm the "restricted to deployment admins" gate; confirmPOST /api/auth/admin/set-rolereturns 404. E2E:bun test packages/e2e/tests/deployment-admin.spec.ts(needs the Postgres+NATS rig).Migration Notes
No DB migration. New env var
DEPLOYMENT_ADMIN_EMAILS(CSV of operator emails) — unset means no deployment admins. Revocation takes effect on next restart.Review Checklist
Summary by cubic
Adds an instance-level deployment admin dashboard at
/_admin, gated byDEPLOYMENT_ADMIN_EMAILS, for operators to search users, impersonate, view orgs, and add members. Secures the surface with CSRF Origin checks, fences rawbetter-authadmin endpoints, forwards realAPIErrorstatuses, and exempts/api/_admin/*from org SSO.New Features
/api/_admin/me,/api/_admin/users?searchValue&limit,/api/_admin/orgs?search&limit,POST /api/_admin/orgs/:orgId/members,POST /api/_admin/impersonate.better-authendpoint directly.Bug Fixes
DEPLOYMENT_ADMIN_EMAILSwith email verification checked after the allowlist (non‑allowlisted get 403; allowlisted unverified getemail_not_verified), reject untrustedOriginon mutating routes (CSRF), fence/api/auth/admin/*(exceptstop-impersonating), mountADMIN_API_PREFIXbefore:org, and exempt/api/_admin/*from org SSO.better-authAPIErrorby shape (name === "APIError"+ numeric 4xx/5xx) to forward status/message/code (nevercause); durable stdout + PostHog events for impersonate and member‑add.Written for commit 0d52f1d. Summary will update on new commits.