Skip to content

Permission-Aware UI

The client receives a ClientSession from GET /auth/session that includes the user’s permissions. UI components use session.has_permission() to conditionally render elements — hiding nav links, buttons, or entire sections that the user cannot access.

src/shared/types/client_session_type.rs
pub struct ClientSession {
pub user_name: String,
pub organization_name: String,
pub is_super_admin: bool,
pub is_owner: bool,
pub permissions: Vec<Permission>,
}
impl ClientSession {
/// Returns true if the user holds a specific permission, or is a super-admin/org owner.
pub fn has_permission(&self, permission: &Permission) -> bool {
self.is_super_admin || self.is_owner || self.permissions.contains(permission)
}
}

has_permission() is the single method for all UI permission checks — super-admins and org owners always return true.

The get_session_api handler maps the server-side Session to a ClientSession:

src/bases/auth/api/get_session_api.rs
Ok(Some(ClientSession {
user_name: session.user.name,
organization_name: session.organization.name,
is_super_admin: session.is_super_admin,
is_owner: session.is_owner,
permissions: session.permissions, // Vec<Permission> — same type, shared crate
}))

The ClientSession is stored in a Dioxus context and consumed by all components that need to gate rendering.

The sidebar (src/shared/components/sidebar.rs) conditionally renders each nav link using session.has_permission():

// Agents link — only visible if the user can list agents
if session.has_permission(&Permission::Agent(
AgentPermission::Collection(AgentCollectionPermission::List)
)) {
NavItem {
icon: rsx! { Bot { size: 20 } },
label: "Agents",
href: "/agents",
is_collapsed: is_collapsed(),
}
}
// Text Agents — same pattern
if session.has_permission(&Permission::TextAgent(
TextAgentPermission::Collection(TextAgentCollectionPermission::List)
)) {
NavItem { label: "Text Agents", href: "/text-agents", /* ... */ }
}

Always visible: Contacts, Settings.

Permission-gated: Dashboard (Dashboard:Instance:ViewOrg or Dashboard:Instance:ViewOwn), Calls (Call:Collection:List), Messaging, Phones, Agents, Knowledge, Analyzers, Text Agents, To-Do Types, To-Dos.

Use the same has_permission() call to gate any action within a view:

src/mods/call/views/call_details_view.rs
let session = use_context::<ClientSession>();
let can_rerun = session.has_permission(&Permission::Call(
CallPermission::Instance(CallInstancePermission::Update)
));
// Only render the button if the user has the permission AND transcription exists
let rerun_actions = if has_transcription && can_rerun {
Some(rsx! { /* Rerun Analysis button */ })
} else {
None
};

When a user navigates to a page they can’t use, render the shared AccessDenied component instead of the page content. It shows a centered card with a shield icon, a title, a message, and optional children (e.g. a link or hint).

src/shared/components/access_denied_component.rs
#[component]
pub fn AccessDenied(
title: String, // e.g. "Dashboard access required"
message: String, // e.g. "You don't have permission to view the dashboard."
children: Element, // Optional extra content below the message
) -> Element

Use it in any view where permission is denied:

use crate::shared::{AccessDenied, ClientSession};
let session = use_context::<ClientSession>();
let can_view = can_view_dashboard(&session);
if !can_view {
return rsx! {
ViewContainer {
title: "Dashboard",
description: "Overview of your organization's activity",
AccessDenied {
title: "Dashboard access required",
message: "You don't have permission to view the dashboard.",
p { class: "text-sm text-muted-foreground",
"Ask your organization owner to grant you a Dashboard permission."
}
}
}
};
}

To restrict what a member sees:

  1. Create a role with only the permissions they need (see ABAC Authorization).
  2. Assign that role to the member’s member_role record.
  3. The next time they log in, get_session_api loads only those permissions into ClientSession.
  4. The sidebar, buttons, and action panels reflect exactly what they’re allowed to do.

Example — Read-Only Analyst role:

A member with only Call:Collection:List and Call:Instance:View:

  • ✅ Sees the Calls nav link
  • ✅ Can open call detail pages
  • ❌ No “Rerun Analysis” button (requires Call:Instance:Update)
  • ❌ No Agents, Knowledge, Analyzers, Text Agents nav links
  • ❌ No To-Dos, To-Do Types nav links
INSERT INTO role (id, organization_id, name, permissions) VALUES (
gen_random_uuid(),
'<org_id>',
'Read-Only Analyst',
'["Call:Collection:List", "Call:Instance:View"]'
);
// 1. Get the session from context
let session = use_context::<ClientSession>();
// 2. Build the permission to check
let can_create = session.has_permission(&Permission::Agent(
AgentPermission::Collection(AgentCollectionPermission::Create)
));
// 3. Conditionally render
if can_create {
rsx! { Button { "Add Agent" } }
}

Import the full permission type hierarchy from crate::bases::auth::* — all resource enums are available there.

src/shared/
├── types/
│ └── client_session_type.rs # ClientSession + has_permission()
└── components/
├── access_denied_component.rs # Shared AccessDenied card
├── sidebar.rs # permission-gated nav links
└── nav_item.rs # NavItem component
src/bases/auth/
└── api/
└── get_session_api.rs # Session → ClientSession mapping