feat: role-based access control for admin dashboard#406
feat: role-based access control for admin dashboard#4060xVida merged 1 commit intoStellar-Fluid:mainfrom
Conversation
|
@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! 🚀 |
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
a198254 to
827d216
Compare
| 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
|
|
||
| // ── 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
| // ── 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
| 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
| 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
| 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
|
|
||
| // ── 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
| // ── 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
| 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
| 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 PR is included in version 1.22.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
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
view_*permissions onlyBackend
AdminUsermodel — stores email, bcrypt passwordHash, role, active flag.POST /admin/auth/login— takes{email, password}, validates against theAdminUsertable first, falls back toADMIN_EMAIL/ADMIN_PASSWORD_HASHenv vars for bootstrap deployments. Returns a signed JWT (HS256, 8-hour TTL) and the user's role.requirePermission(permission)middleware — readsx-admin-jwtheader (verified JWT → role from payload) and falls back tox-admin-tokenstatic token asSUPER_ADMINfor backward compatibility. Existing scripts and API clients usingFLUID_ADMIN_TOKENcontinue 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 tomanage_users(SUPER_ADMIN only).Key routes updated to use
requirePermissionmiddleware: api-keys, signers, tenants/subscription-tiers, transactions, analytics, fee-multiplier, audit-log, webhooks/dlq.Frontend
auth.tscallsPOST /admin/auth/loginon 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/userspage 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.tsshares role/permission constants between components./api/admin/usersforward requests to the backend with the session JWT attached.Tests (38 passing)
Test output:
closes #208