RBAC Dashboard Design
Overview
The Role-Based Access Control (RBAC) dashboard is a comprehensive interface within do.dev for managing users, roles, permissions, organizations, and teams. All new users start with a "waitlist" role and must be approved by administrators.
Dashboard Architecture
graph TB
subgraph "RBAC Dashboard"
MAIN[RBAC Main View]
WAITLIST[Waitlist Management]
ROLES[Role Management]
PERMS[Permission Matrix]
USERS[User Management]
ORGS[Organization Management]
TEAMS[Team Management]
AUDIT[Audit Logs]
end
subgraph "Core Features"
APPROVE[Approve/Reject Users]
CREATE[Create Roles]
ASSIGN[Assign Permissions]
INVITE[Invite Members]
MONITOR[Monitor Activity]
end
MAIN --> WAITLIST
MAIN --> ROLES
MAIN --> PERMS
MAIN --> USERS
MAIN --> ORGS
MAIN --> TEAMS
MAIN --> AUDIT
WAITLIST --> APPROVE
ROLES --> CREATE
PERMS --> ASSIGN
ORGS --> INVITE
AUDIT --> MONITORUI Components
1. Waitlist Management
// Waitlist dashboard component
export function WaitlistDashboard() {
return (
<div className="space-y-6">
<WaitlistStats />
<WaitlistQueue />
<BulkActions />
<ApprovalHistory />
</div>
)
}
// Waitlist statistics
export function WaitlistStats() {
return (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatCard
title="Pending Users"
value={pendingCount}
trend="+12% from last week"
icon={<UserPlus />}
/>
<StatCard
title="Approved Today"
value={approvedToday}
trend="5 users"
icon={<CheckCircle />}
/>
<StatCard
title="Rejected"
value={rejectedCount}
trend="2 users"
icon={<XCircle />}
/>
<StatCard
title="Avg Wait Time"
value="2.5 days"
trend="-0.5 days"
icon={<Clock />}
/>
</div>
)
}
// Waitlist queue with filtering
export function WaitlistQueue() {
return (
<Card>
<CardHeader>
<CardTitle>Waitlist Queue</CardTitle>
<div className="flex gap-2">
<SearchInput placeholder="Search users..." />
<FilterDropdown
options={[
{ label: "All", value: "all" },
{ label: "New Today", value: "today" },
{ label: "This Week", value: "week" },
{ label: "Has Organization", value: "org" },
]}
/>
<SortDropdown
options={[
{ label: "Newest First", value: "-created" },
{ label: "Oldest First", value: "created" },
{ label: "By Email", value: "email" },
]}
/>
</div>
</CardHeader>
<CardContent>
<WaitlistTable />
</CardContent>
</Card>
)
}
// Individual waitlist entry
interface WaitlistEntry {
id: string
email: string
name: string
createdAt: Date
source: string
referrer?: string
notes?: string
riskScore: number
emailVerified: boolean
}
// Waitlist table with actions
export function WaitlistTable() {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>
<Checkbox /> {/* Select all */}
</TableHead>
<TableHead>User</TableHead>
<TableHead>Registration</TableHead>
<TableHead>Source</TableHead>
<TableHead>Risk Score</TableHead>
<TableHead>Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{waitlistUsers.map((user) => (
<TableRow key={user.id}>
<TableCell>
<Checkbox />
</TableCell>
<TableCell>
<div className="flex items-center gap-3">
<Avatar>
<AvatarImage src={user.avatar} />
<AvatarFallback>{getInitials(user.name)}</AvatarFallback>
</Avatar>
<div>
<div className="font-medium">{user.name}</div>
<div className="text-sm text-muted-foreground">{user.email}</div>
{!user.emailVerified && (
<Badge variant="warning" size="sm">Unverified</Badge>
)}
</div>
</div>
</TableCell>
<TableCell>
<div className="text-sm">
<div>{formatDate(user.createdAt)}</div>
<div className="text-muted-foreground">
{formatRelativeTime(user.createdAt)}
</div>
</div>
</TableCell>
<TableCell>
<Badge variant="outline">{user.source}</Badge>
{user.referrer && (
<div className="text-xs text-muted-foreground mt-1">
via {user.referrer}
</div>
)}
</TableCell>
<TableCell>
<RiskScoreBadge score={user.riskScore} />
</TableCell>
<TableCell>
<div className="flex gap-2">
<Button
size="sm"
variant="default"
onClick={() => approveUser(user.id)}
>
Approve
</Button>
<Button
size="sm"
variant="outline"
onClick={() => viewUserDetails(user.id)}
>
View
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button size="sm" variant="ghost">
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem onClick={() => addNote(user.id)}>
Add Note
</DropdownMenuItem>
<DropdownMenuItem onClick={() => requestMoreInfo(user.id)}>
Request Info
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => rejectUser(user.id)}
className="text-destructive"
>
Reject
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)
}2. Role Management Interface
// Role management dashboard
export function RoleManagementDashboard() {
return (
<div className="space-y-6">
<RoleOverview />
<RoleHierarchy />
<RolesList />
<PermissionAssignment />
</div>
)
}
// Visual role hierarchy
export function RoleHierarchy() {
return (
<Card>
<CardHeader>
<CardTitle>Role Hierarchy</CardTitle>
<CardDescription>
Visual representation of role inheritance and permissions
</CardDescription>
</CardHeader>
<CardContent>
<div className="relative">
{/* Interactive tree view of roles */}
<RoleTree
roles={roles}
onRoleClick={(role) => setSelectedRole(role)}
onPermissionDrop={(roleId, permissionId) =>
assignPermission(roleId, permissionId)
}
/>
</div>
</CardContent>
</Card>
)
}
// Role creation form
export function CreateRoleDialog() {
return (
<Dialog>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Role</DialogTitle>
</DialogHeader>
<form onSubmit={handleCreateRole}>
<div className="space-y-4">
<div>
<Label htmlFor="name">Role Name</Label>
<Input
id="name"
placeholder="e.g., Content Editor"
value={roleName}
onChange={(e) => setRoleName(e.target.value)}
/>
</div>
<div>
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
placeholder="Describe the role's purpose..."
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
</div>
<div>
<Label>Scope</Label>
<RadioGroup value={scope} onValueChange={setScope}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="global" id="global" />
<Label htmlFor="global">Global (Platform-wide)</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="organization" id="organization" />
<Label htmlFor="organization">Organization</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="team" id="team" />
<Label htmlFor="team">Team</Label>
</div>
</RadioGroup>
</div>
<div>
<Label>Inherits From</Label>
<Select value={inheritsFrom} onValueChange={setInheritsFrom}>
<SelectTrigger>
<SelectValue placeholder="Select parent role..." />
</SelectTrigger>
<SelectContent>
{availableParentRoles.map((role) => (
<SelectItem key={role.id} value={role.id}>
{role.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Initial Permissions</Label>
<PermissionPicker
selectedPermissions={selectedPermissions}
onChange={setSelectedPermissions}
/>
</div>
</div>
<DialogFooter>
<Button type="submit">Create Role</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)
}3. Permission Matrix
// Visual permission matrix
export function PermissionMatrix() {
return (
<Card>
<CardHeader>
<CardTitle>Permission Matrix</CardTitle>
<div className="flex items-center gap-4">
<SearchInput placeholder="Search permissions..." />
<Button variant="outline" size="sm">
<Download className="h-4 w-4 mr-2" />
Export
</Button>
</div>
</CardHeader>
<CardContent>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr>
<th className="sticky left-0 bg-background p-2 text-left">
Resource
</th>
{roles.map((role) => (
<th
key={role.id}
className="p-2 text-center min-w-[100px] cursor-pointer hover:bg-muted"
onClick={() => selectRole(role.id)}
>
<div className="font-medium">{role.name}</div>
<div className="text-xs text-muted-foreground">
{role.userCount} users
</div>
</th>
))}
</tr>
</thead>
<tbody>
{resources.map((resource) => (
<tr key={resource.id}>
<td className="sticky left-0 bg-background p-2 font-medium">
{resource.name}
</td>
{roles.map((role) => (
<td key={`${resource.id}-${role.id}`} className="p-2">
<PermissionCell
resource={resource}
role={role}
permissions={getPermissions(resource.id, role.id)}
onChange={(permissions) =>
updatePermissions(resource.id, role.id, permissions)
}
/>
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</CardContent>
</Card>
)
}
// Individual permission cell
export function PermissionCell({ resource, role, permissions, onChange }) {
const actions = ['create', 'read', 'update', 'delete']
return (
<div className="flex justify-center gap-1">
{actions.map((action) => (
<Tooltip key={action}>
<TooltipTrigger>
<button
className={cn(
"w-6 h-6 rounded text-xs font-medium",
permissions.includes(action)
? "bg-green-500 text-white"
: "bg-gray-200 text-gray-400"
)}
onClick={() => togglePermission(action)}
>
{action[0].toUpperCase()}
</button>
</TooltipTrigger>
<TooltipContent>
{action} {resource.name}
</TooltipContent>
</Tooltip>
))}
</div>
)
}4. Organization Management
// Organization dashboard
export function OrganizationManagementDashboard() {
return (
<div className="space-y-6">
<OrganizationStats />
<OrganizationList />
<TeamOverview />
<InvitationManager />
</div>
)
}
// Organization creation wizard
export function CreateOrganizationWizard() {
const [step, setStep] = useState(1)
return (
<Card>
<CardHeader>
<CardTitle>Create Organization</CardTitle>
<Progress value={(step / 4) * 100} />
</CardHeader>
<CardContent>
{step === 1 && <BasicInfoStep />}
{step === 2 && <PlanSelectionStep />}
{step === 3 && <TeamSetupStep />}
{step === 4 && <ReviewStep />}
</CardContent>
<CardFooter className="flex justify-between">
<Button
variant="outline"
onClick={() => setStep(step - 1)}
disabled={step === 1}
>
Previous
</Button>
<Button
onClick={() => step === 4 ? createOrganization() : setStep(step + 1)}
>
{step === 4 ? 'Create Organization' : 'Next'}
</Button>
</CardFooter>
</Card>
)
}
// Organization member management
export function OrganizationMembers({ organizationId }) {
return (
<Card>
<CardHeader>
<CardTitle>Organization Members</CardTitle>
<div className="flex gap-2">
<Button onClick={openInviteDialog}>
<UserPlus className="h-4 w-4 mr-2" />
Invite Members
</Button>
<Button variant="outline">
<Upload className="h-4 w-4 mr-2" />
Bulk Import
</Button>
</div>
</CardHeader>
<CardContent>
<MemberTable
members={members}
onRoleChange={(memberId, newRole) =>
updateMemberRole(organizationId, memberId, newRole)
}
onRemove={(memberId) =>
removeMember(organizationId, memberId)
}
/>
</CardContent>
</Card>
)
}5. Audit Log Interface
// Audit log viewer
export function AuditLogDashboard() {
return (
<div className="space-y-6">
<AuditLogFilters />
<AuditLogTimeline />
<AuditLogDetails />
</div>
)
}
// Audit log entry
interface AuditLogEntry {
id: string
timestamp: Date
actor: {
id: string
name: string
email: string
role: string
}
action: string
resource: {
type: string
id: string
name: string
}
changes?: {
field: string
oldValue: any
newValue: any
}[]
ipAddress: string
userAgent: string
organizationId?: string
teamId?: string
}
// Audit log timeline view
export function AuditLogTimeline() {
return (
<Card>
<CardHeader>
<CardTitle>Activity Timeline</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
{auditLogs.map((log, index) => (
<div key={log.id} className="flex gap-4">
<div className="flex flex-col items-center">
<div className={cn(
"w-10 h-10 rounded-full flex items-center justify-center",
getActionColor(log.action)
)}>
{getActionIcon(log.action)}
</div>
{index < auditLogs.length - 1 && (
<div className="w-0.5 h-full bg-gray-200 mt-2" />
)}
</div>
<div className="flex-1 pb-8">
<div className="flex items-center justify-between">
<div>
<p className="font-medium">
{log.actor.name} {getActionDescription(log)}
</p>
<p className="text-sm text-muted-foreground">
{formatRelativeTime(log.timestamp)}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => viewLogDetails(log.id)}
>
View Details
</Button>
</div>
{log.changes && (
<div className="mt-2 p-3 bg-muted rounded-md">
<p className="text-sm font-medium mb-1">Changes:</p>
{log.changes.map((change, i) => (
<div key={i} className="text-sm">
<span className="text-muted-foreground">
{change.field}:
</span>{" "}
<span className="line-through text-red-600">
{formatValue(change.oldValue)}
</span>{" "}
→{" "}
<span className="text-green-600">
{formatValue(change.newValue)}
</span>
</div>
))}
</div>
)}
</div>
</div>
))}
</div>
</CardContent>
</Card>
)
}API Endpoints
Waitlist Management
// Waitlist API endpoints
POST /api/admin/waitlist/approve
POST /api/admin/waitlist/reject
POST /api/admin/waitlist/bulk-approve
GET /api/admin/waitlist
GET /api/admin/waitlist/stats
POST /api/admin/waitlist/:userId/noteRole Management
// Role CRUD operations
GET /api/admin/roles
POST /api/admin/roles
PUT /api/admin/roles/:roleId
DELETE /api/admin/roles/:roleId
// Permission management
GET /api/admin/permissions
POST /api/admin/roles/:roleId/permissions
DELETE /api/admin/roles/:roleId/permissions/:permissionId
// User role assignment
POST /api/admin/users/:userId/roles
DELETE /api/admin/users/:userId/roles/:roleIdOrganization Management
// Organization operations
GET /api/organizations
POST /api/organizations
PUT /api/organizations/:orgId
DELETE /api/organizations/:orgId
// Member management
GET /api/organizations/:orgId/members
POST /api/organizations/:orgId/invitations
PUT /api/organizations/:orgId/members/:userId/role
DELETE /api/organizations/:orgId/members/:userId
// Team operations
GET /api/organizations/:orgId/teams
POST /api/organizations/:orgId/teams
PUT /api/teams/:teamId
DELETE /api/teams/:teamIdAudit Logs
// Audit log queries
GET /api/admin/audit-logs
GET /api/admin/audit-logs/:logId
POST /api/admin/audit-logs/exportSecurity Considerations
Access Control
- Only super admins can access the full RBAC dashboard
- Organization admins can manage their organization's roles and members
- Team admins can manage team members
- All actions are logged in the audit trail
Data Protection
- Sensitive operations require re-authentication
- IP-based restrictions for admin access
- Rate limiting on bulk operations
- Encrypted audit logs
Compliance
- GDPR-compliant data handling
- Role change notifications
- Audit log retention policies
- Data export capabilities
Performance Optimization
Caching Strategy
// Cache configuration
const cacheConfig = {
roles: {
ttl: 300, // 5 minutes
key: (orgId?: string) => `roles:${orgId || 'global'}`
},
permissions: {
ttl: 600, // 10 minutes
key: (roleId: string) => `permissions:${roleId}`
},
waitlist: {
ttl: 60, // 1 minute
key: () => 'waitlist:pending'
}
}Pagination
All list views support pagination:
interface PaginationParams {
page: number
limit: number
sort?: string
filter?: Record<string, any>
}
interface PaginatedResponse<T> {
data: T[]
total: number
page: number
limit: number
hasMore: boolean
}Real-time Updates
// WebSocket events for real-time updates
const wsEvents = {
'waitlist:user:added': (user: WaitlistUser) => void
'waitlist:user:approved': (userId: string) => void
'role:created': (role: Role) => void
'role:updated': (role: Role) => void
'permission:assigned': (data: { roleId: string, permission: Permission }) => void
'organization:member:added': (data: { orgId: string, member: Member }) => void
}Mobile Responsiveness
The RBAC dashboard is fully responsive:
- Collapsible sidebar on mobile
- Touch-friendly controls
- Swipeable actions
- Optimized data tables for small screens
- Progressive disclosure of complex forms
Accessibility
- WCAG 2.1 AA compliance
- Keyboard navigation support
- Screen reader announcements
- High contrast mode support
- Focus indicators
- ARIA labels and descriptions