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.
ClientSession
Section titled “ClientSession”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.
How the Session Reaches the UI
Section titled “How the Session Reaches the UI”The get_session_api handler maps the server-side Session to a ClientSession:
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.
Hiding Nav Links
Section titled “Hiding Nav Links”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 agentsif 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 patternif 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.
Hiding Buttons and Actions
Section titled “Hiding Buttons and Actions”Use the same has_permission() call to gate any action within a view:
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 existslet rerun_actions = if has_transcription && can_rerun { Some(rsx! { /* Rerun Analysis button */ })} else { None};AccessDenied Component
Section titled “AccessDenied Component”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).
#[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) -> ElementUse 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." } } } };}Limiting a User in the UI
Section titled “Limiting a User in the UI”To restrict what a member sees:
- Create a role with only the permissions they need (see ABAC Authorization).
- Assign that role to the member’s
member_rolerecord. - The next time they log in,
get_session_apiloads only those permissions intoClientSession. - 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"]');Adding Permission Gates to New Components
Section titled “Adding Permission Gates to New Components”// 1. Get the session from contextlet session = use_context::<ClientSession>();
// 2. Build the permission to checklet can_create = session.has_permission(&Permission::Agent( AgentPermission::Collection(AgentCollectionPermission::Create)));
// 3. Conditionally renderif can_create { rsx! { Button { "Add Agent" } }}Import the full permission type hierarchy from crate::bases::auth::* — all resource enums are available there.
Module Structure
Section titled “Module Structure”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