Add admin impersonation console to the dashboard - #1321
Open
Sharqiewicz wants to merge 52 commits into
Open
Conversation
✅ Deploy Preview for vortex-sandbox ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for vortexfi canceled.
|
✅ Deploy Preview for vrtx-dashboard ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Contributor
There was a problem hiding this comment.
Pull request overview
Adds a high-risk admin impersonation console across the API and dashboard; despite the title, subaccounts remain deferred.
Changes:
- Adds role-gated impersonation sessions and principal substitution.
- Adds dashboard account search, auditing, and “Log in as” flows.
- Adds migrations, tests, operational tooling, and security documentation.
Reviewed changes
Copilot reviewed 55 out of 56 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
docs/security-spec/RISK-REGISTER.md |
Records impersonation risk. |
docs/security-spec/README.md |
Indexes the new specification. |
docs/security-spec/01-auth/admin-impersonation.md |
Defines impersonation security controls. |
docs/security-spec/01-auth/admin-auth.md |
Separates both admin surfaces. |
docs/product-dashboard.md |
Documents operator UI behavior. |
docs/architecture-identity-model.md |
Documents principal substitution. |
apps/dashboard/src/types/bun-test.d.ts |
Declares Bun mocking types. |
apps/dashboard/src/stores/impersonation.store.ts |
Manages impersonation state. |
apps/dashboard/src/stores/impersonation.store.test.ts |
Tests session transitions. |
apps/dashboard/src/stores/auth.store.ts |
Exports account-state cleanup. |
apps/dashboard/src/services/auth.ts |
Stores impersonation credentials. |
apps/dashboard/src/services/auth.test.ts |
Tests token precedence. |
apps/dashboard/src/services/api/api-client.ts |
Sends impersonation bearer tokens. |
apps/dashboard/src/services/api/api-client.test.ts |
Tests impersonated requests. |
apps/dashboard/src/services/api/admin-console.service.ts |
Defines admin API client. |
apps/dashboard/src/routeTree.gen.ts |
Registers admin routes. |
apps/dashboard/src/routes/_app/admin.tsx |
Adds account search and audit UI. |
apps/dashboard/src/routes/_app/admin.$profileId.tsx |
Adds account details. |
apps/dashboard/src/routes/_app.tsx |
Mounts impersonation banner. |
apps/dashboard/src/hooks/useAdminConsole.ts |
Adds admin query hooks. |
apps/dashboard/src/components/layout/ImpersonationBanner.tsx |
Displays active-session warning. |
apps/dashboard/src/components/layout/AppSidebar.tsx |
Adds role-gated navigation. |
apps/dashboard/src/components/admin/ImpersonateDialog.tsx |
Confirms impersonation starts. |
apps/dashboard/src/components/admin/AdminAccountsTable.tsx |
Displays searchable accounts. |
apps/api/src/models/profileRole.model.ts |
Adds vortex_admin. |
apps/api/src/models/index.ts |
Registers session associations. |
apps/api/src/models/adminImpersonationSession.model.ts |
Models impersonation sessions. |
apps/api/src/database/migrations/060-create-admin-impersonation-sessions.ts |
Creates session storage. |
apps/api/src/database/migrations/059-allow-vortex-admin-profile-role.ts |
Extends role constraints. |
apps/api/src/config/vars.ts |
Adds the kill switch. |
apps/api/src/api/services/impersonation.service.ts |
Implements session lifecycle. |
apps/api/src/api/services/impersonation.service.test.ts |
Tests lifecycle invariants. |
apps/api/src/api/routes/v1/index.ts |
Mounts admin-console routes. |
apps/api/src/api/routes/v1/api-credentials.route.ts |
Blocks credential minting. |
apps/api/src/api/routes/v1/api-credentials.route.test.ts |
Tests credential blocking. |
apps/api/src/api/routes/v1/admin-console/impersonation.route.ts |
Defines session routes. |
apps/api/src/api/routes/v1/admin-console/admin-console.route.test.ts |
Tests admin-console authorization. |
apps/api/src/api/routes/v1/admin-console/accounts.route.ts |
Defines account routes. |
apps/api/src/api/observability/types.ts |
Adds impersonation operations. |
apps/api/src/api/observability/apiClientEvent.service.ts |
Adds operator attribution. |
apps/api/src/api/observability/apiClientEvent.service.test.ts |
Tests attribution metadata. |
apps/api/src/api/middlewares/vortexAdminAuth.ts |
Adds role authorization. |
apps/api/src/api/middlewares/vortexAdminAuth.test.ts |
Tests role authorization. |
apps/api/src/api/middlewares/supabaseAuth.ts |
Resolves substituted principals. |
apps/api/src/api/middlewares/supabaseAuth.impersonation.test.ts |
Tests Supabase impersonation. |
apps/api/src/api/middlewares/ownershipAuth.impersonation.test.ts |
Tests target ownership. |
apps/api/src/api/middlewares/dualAuth.ts |
Supports impersonation bearer tokens. |
apps/api/src/api/middlewares/bearerPrincipal.ts |
Centralizes bearer resolution. |
apps/api/src/api/middlewares/bearerPrincipal.test.ts |
Tests principal resolution. |
apps/api/src/api/controllers/admin/profileRoles.controller.ts |
Restricts HTTP role grants. |
apps/api/src/api/controllers/admin/profileRoles.controller.test.ts |
Tests role restrictions. |
apps/api/src/api/controllers/admin-console/impersonation.controller.ts |
Implements session endpoints. |
apps/api/src/api/controllers/admin-console/accounts.controller.ts |
Implements account queries. |
apps/api/scripts/grant-vortex-admin.ts |
Adds out-of-band role grant. |
apps/api/package.json |
Registers the grant command. |
apps/api/.env.example |
Documents the kill switch. |
The corridor and customer-type resolvers preferred the query string while the Alfredpay POST handlers read the body, so a manager could present an allowed corridor in the query and have the operation run against a different one in the body, defeating the per-manager corridor grant.
Alfredpay identifies customers by email, so a creation conflict was resolved by adopting whatever customer that email returned. A managed child's contact email is chosen by its manager and never verified, so a manager could point a child at an email whose customer is already verified and inherit that KYC state. Adoption now requires the customer to be unclaimed or already ours, which still covers the retry case where an earlier creation reached Alfredpay but failed to persist.
parseAPIError still matched the pre-PR Mykobo message and a BRL message the backend retired, so both fell through to a generic error and consumers branching on MykoboKycRequiredError or BrlKycStatusError stopped seeing them.
Moving session routes onto requirePartnerOrUserAuth lost the 503 that tells a caller the provider is briefly unreachable rather than their token being rejected: the Bearer branch had no handler, so a transient failure reached the error converter as a 500.
Revoking an already-revoked child credential rewrote its revocation timestamp, losing when the credential actually stopped being valid; revocation stays idempotent but only the first call writes. Provisioning ignored the manager's allowedCustomerTypes narrowing, creating children the manager could never operate — a mismatched child now only arises by tightening policy after the child exists, which is how the delegated tests set that state up. The admin provisioning route mapped only one of the two error classes createManagedProfile raises, so a conflict surfaced as a 500.
The ADR declares the external subject id immutable but only contact_email had a trigger. The corridor grant CHECK is left as-is on purpose: an empty array is how every corridor is revoked from an active manager, and the schema test now pins that.
Webhooks register against the manager's own credential scope, so a child selection silently produced a subscription the caller did not ask for.
Provisioning locks the manager row and deletion did not, so a concurrent re-provision could observe a child mid-deletion.
Every public and secret key validation ran a second query just to learn whether the credential belongs to a managed profile, on the hottest authenticated paths.
The partner-facing pages are whitelabeled, and this line introduced provider brand names to describe which routes accept a managed child.
# Conflicts: # apps/api/src/api/middlewares/dualAuth.ts # docs/architecture-identity-model.md # docs/security-spec/01-auth/admin-auth.md # docs/security-spec/RISK-REGISTER.md
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.
Summary
Adds an operator-facing admin console and short-lived impersonation sessions for profiles holding the out-of-band
vortex_adminrole.The implementation keeps the operator's Supabase session separate, stores only a hash of the temporary impersonation token on the server, attributes impersonated API events to both the operator and target, prevents impersonation chaining and API-credential minting, and supports revocation, expiry, role-removal invalidation, and a global kill switch.
Current implementation
Security posture and agreed first-iteration scope
Important
The current branch resolves an impersonation token as the target profile with the target's full application permissions. This is not the intended final scope for the first production iteration.
The first iteration must provide a server-enforced read-only support mode. Hiding buttons in the dashboard is not an authorization boundary: the API must default-deny mutations while
req.impersonationis present and allow only an explicit set of support reads.Expected read-only capabilities include:
The impersonated principal must not be able to:
Required first-iteration follow-up
Additional hardening before production use
localStorage, preferably behind an HttpOnly secure cookie or backend-for-frontend session.Verification completed for the current implementation