v1.0.25: Closing the Admin Door — RBAC Guard & Live Dashboard Data
v1.0.25: Closing the Admin Door — RBAC Guard & Live Dashboard Data
What Happened
During a routine audit of the dashboard routing layer, a critical gap was identified in src/app/dashboard/admin/page.tsx: the admin route had no access control guard.
A code comment on lines 12–14 explicitly flagged the issue:
// In production, check RBAC role here via lib/auth.ts
…but the check itself was commented out and never implemented. This meant any user who had successfully authenticated — regardless of whether they were a gym member, a coach, or an owner — could directly visit /dashboard/admin and land on the admin dashboard without being challenged.
On top of that, the stats cards on that page were all hardcoded to 0 and the audit log was entirely empty, meaning even legitimate admins were looking at a non-functional dashboard.
Both issues are resolved in v1.0.25.
The Fix: Role-Based Access Control
The RBAC guard is now live. Immediately after the user session is resolved, the server queries the orgMembers table to check whether the requesting user holds an admin or owner role within the active organisation. Anyone who doesn't meet that criteria is silently redirected to /dashboard — they never see an error page, and they never see admin data.
const member = await db
.select()
.from(orgMembers)
.where(
and(
eq(orgMembers.userId, session.user.id),
eq(orgMembers.orgId, activeOrgId)
)
);
if (!member || (member.role !== 'admin' && member.role !== 'owner')) {
redirect('/dashboard');
}
This pattern is consistent with how access control should work across all privileged routes: resolve the session, verify the role against the database, redirect on failure — all server-side, before any sensitive data is touched.
Live Stats Cards
With the guard in place, it also made sense to fix the dashboard itself. The four stats cards are now backed by real database queries:
| Card | Data Source |
|---|---|
| Organisations | Count of all organisation records |
| Users | Count of all user records |
| Active Subscriptions | Count of subscriptions with active status |
| Audit Events | Count of entries in the audit log |
The audit log table is similarly now populated from live records rather than rendering an empty state by default.
Who Is Affected
- Self-hosted deployments: Ensure your
orgMemberstable has arolecolumn before deploying v1.0.25. If you are running migrations, this column should already be present from earlier schema versions. - Cloud-hosted tenants: No action required. The fix is deployed automatically.
- Members and coaches: No visible change. The redirect is seamless.
- Admins and owners: The admin dashboard will now show accurate, real-time platform statistics.
Severity Assessment
This was a high-severity access control omission. Although the data exposed on the admin page was not sensitive personal data (stats cards were hardcoded to zero), the route itself was unguarded — a pattern that could have been exploited as the page was developed further and real data was wired in. Patching it at this stage, before the page carried live data, was the right call.
What's Next
We will be conducting a sweep of all remaining dashboard routes to verify that every privileged page has a server-side RBAC guard in place before data queries are executed. Any route found without a guard will be flagged and patched in a follow-up release.