Skip to content

feat: role-based access control for admin dashboard#406

Merged
0xVida merged 1 commit intoStellar-Fluid:mainfrom
daveades:feat/rbac-admin-dashboard
Mar 30, 2026
Merged

feat: role-based access control for admin dashboard#406
0xVida merged 1 commit intoStellar-Fluid:mainfrom
daveades:feat/rbac-admin-dashboard

Conversation

@daveades
Copy link
Copy Markdown
Contributor

What

Implements a four-role RBAC system for the admin dashboard so different team members get granular access instead of sharing a single admin account.

Roles and permissions

Role Permissions
SUPER_ADMIN Everything including user management
ADMIN Full operational control (API keys, signers, config, SAR, tenants) — no user or billing management
READ_ONLY All view_* permissions only
BILLING Billing operations + transaction and tenant views

Backend

AdminUser model — stores email, bcrypt passwordHash, role, active flag.

POST /admin/auth/login — takes {email, password}, validates against the AdminUser table first, falls back to ADMIN_EMAIL/ADMIN_PASSWORD_HASH env vars for bootstrap deployments. Returns a signed JWT (HS256, 8-hour TTL) and the user's role.

requirePermission(permission) middleware — reads x-admin-jwt header (verified JWT → role from payload) and falls back to x-admin-token static token as SUPER_ADMIN for backward compatibility. Existing scripts and API clients using FLUID_ADMIN_TOKEN continue working unchanged.

If a JWT header is present but invalid, the request is rejected as 401 — it does not silently fall through to the static token, preventing privilege escalation.

Admin user CRUD (/admin/users) — list, create, update role, deactivate — all gated to manage_users (SUPER_ADMIN only).

Key routes updated to use requirePermission middleware: api-keys, signers, tenants/subscription-tiers, transactions, analytics, fee-multiplier, audit-log, webhooks/dlq.

Frontend

  • auth.ts calls POST /admin/auth/login on the backend during NextAuth credential check; stores the backend JWT and role in the session. Falls back to env-var auth when the backend is unreachable.
  • /admin/users page with a full user management table: create user modal (email, password, role), inline role change dropdown, deactivate button — all restricted to SUPER_ADMIN in the UI.
  • lib/permissions.ts shares role/permission constants between components.
  • API proxy routes at /api/admin/users forward requests to the backend with the session JWT attached.

Tests (38 passing)

src/utils/permissions.test.ts   — role/permission matrix, superset checks, READ_ONLY purity
src/utils/adminAuth.test.ts     — JWT round-trip, tamper detection, requirePermission for
                                   each role (ADMIN 403 on manage_users, BILLING 403 on
                                   manage_api_keys, SUPER_ADMIN 200 on all, 401 on no auth)
src/handlers/adminUsers.test.ts — all five handler unit tests incl. 409 duplicate,
                                   404 not-found, passwordHash not leaked in responses

Test output:

 Test Files  3 passed (3)
      Tests  38 passed (38)

closes #208

@drips-wave
Copy link
Copy Markdown

drips-wave bot commented Mar 30, 2026

@daveades Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

Add a four-role RBAC system (SUPER_ADMIN, ADMIN, READ_ONLY, BILLING) to
the admin dashboard and Express backend.

Backend:
- AdminUser model in Prisma with email, bcrypt passwordHash, role, active
- permissions.ts defines Permission constants and per-role permission sets
- adminAuth.ts: signAdminJwt/verifyAdminJwt helpers and requirePermission()
  Express middleware factory; falls back to static FLUID_ADMIN_TOKEN as
  SUPER_ADMIN for backward compatibility
- POST /admin/auth/login issues signed JWTs (HS256, 8h TTL); falls back
  to ADMIN_EMAIL/ADMIN_PASSWORD_HASH env vars for bootstrap deployments
- CRUD routes for admin users (GET/POST/PATCH role/DELETE) all gated by
  requirePermission("manage_users")
- Key admin routes updated to use requirePermission middleware:
  api-keys, signers, tenants, transactions, analytics, config, audit-logs

Frontend:
- lib/permissions.ts shares role/permission constants with the UI
- auth.ts calls backend /admin/auth/login first; env-var fallback retained
  for single-admin deployments; role and adminJwt stored in session
- /admin/users page with AdminUsersTable: create user modal, inline role
  change dropdown, deactivate button (all gated to SUPER_ADMIN in UI)
- API proxy routes: GET/POST /api/admin/users, PATCH role, DELETE

Tests (38 passing):
- permissions.test.ts: role/permission matrix correctness
- adminAuth.test.ts: JWT round-trip, tamper detection, requirePermission
  enforcement for each role including 401/403 cases
- adminUsers.test.ts: handler unit tests for all five endpoints

closes Stellar-Fluid#208
@daveades daveades force-pushed the feat/rbac-admin-dashboard branch from a198254 to 827d216 Compare March 30, 2026 14:03
app.delete("/admin/users/:id", requirePermission("manage_users"), deactivateAdminUserHandler);

// ── API keys ──────────────────────────────────────────────────────────────────
app.get("/admin/api-keys", requirePermission("view_api_keys"), listApiKeysHandler);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

// ── API keys ──────────────────────────────────────────────────────────────────
app.get("/admin/api-keys", requirePermission("view_api_keys"), listApiKeysHandler);
app.post("/admin/api-keys", requirePermission("manage_api_keys"), upsertApiKeyHandler);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
// ── API keys ──────────────────────────────────────────────────────────────────
app.get("/admin/api-keys", requirePermission("view_api_keys"), listApiKeysHandler);
app.post("/admin/api-keys", requirePermission("manage_api_keys"), upsertApiKeyHandler);
app.patch("/admin/api-keys/:key/revoke", requirePermission("manage_api_keys"), revokeApiKeyHandler);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
app.get("/admin/api-keys", requirePermission("view_api_keys"), listApiKeysHandler);
app.post("/admin/api-keys", requirePermission("manage_api_keys"), upsertApiKeyHandler);
app.patch("/admin/api-keys/:key/revoke", requirePermission("manage_api_keys"), revokeApiKeyHandler);
app.patch("/admin/api-keys/:key/chains", requirePermission("manage_api_keys"), updateApiKeyChainsHandler);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
app.post("/admin/api-keys", requirePermission("manage_api_keys"), upsertApiKeyHandler);
app.patch("/admin/api-keys/:key/revoke", requirePermission("manage_api_keys"), revokeApiKeyHandler);
app.patch("/admin/api-keys/:key/chains", requirePermission("manage_api_keys"), updateApiKeyChainsHandler);
app.delete("/admin/api-keys/:key", requirePermission("manage_api_keys"), revokeApiKeyHandler);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
app.delete("/admin/signers/:publicKey", removeSignerHandler(config));

// ── Signers ───────────────────────────────────────────────────────────────────
app.get("/admin/signers", requirePermission("view_signers"), listSignersHandler(config));

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.

// ── Signers ───────────────────────────────────────────────────────────────────
app.get("/admin/signers", requirePermission("view_signers"), listSignersHandler(config));
app.post("/admin/signers", requirePermission("manage_signers"), addSignerHandler(config));

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
// ── Signers ───────────────────────────────────────────────────────────────────
app.get("/admin/signers", requirePermission("view_signers"), listSignersHandler(config));
app.post("/admin/signers", requirePermission("manage_signers"), addSignerHandler(config));
app.delete("/admin/signers/:publicKey", requirePermission("manage_signers"), removeSignerHandler(config));

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
app.get("/admin/transactions", requirePermission("view_transactions"), listTransactionsHandler);
app.get("/admin/analytics/spend-forecast", requirePermission("view_analytics"), getSpendForecastHandler(config));
app.get("/admin/fee-multiplier", requirePermission("manage_config"), getFeeMultiplierHandler);
app.get("/admin/multi-chain/stats", requirePermission("view_analytics"), getMultiChainStatsHandler(config));

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
app.get("/admin/webhooks/dlq", requirePermission("view_transactions"), listDlqHandler);
app.post("/admin/webhooks/dlq/replay", requirePermission("manage_config"), replayDlqHandler);
app.post("/admin/webhooks/dlq/delete", requirePermission("manage_config"), deleteDlqHandler);
app.get("/admin/audit-log/export", requirePermission("view_audit_logs"), exportAuditLogHandler);

Check failure

Code scanning / CodeQL

Missing rate limiting High

This route handler performs
authorization
, but is not rate-limited.
@0xVida 0xVida merged commit dd5115f into Stellar-Fluid:main Mar 30, 2026
7 of 11 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 1.22.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Role-Based Access Control (RBAC) for Admin Dashboard

3 participants