Member Management
The Settings > Members tab provides organization owners with a full member management interface. You can assign phone numbers, toggle roles, promote or demote owners, and remove members — all from an inline detail panel.
Data Model
Section titled “Data Model”MemberPhoneSummary
Section titled “MemberPhoneSummary”Represents a phone number assigned to a member.
pub struct MemberPhoneSummary { pub phone_number_id: Uuid, pub number: String, // E.164: "+12345678901" pub friendly_name: Option<String>, // "Main Office" pub is_default: bool,}AssignPhonesData
Section titled “AssignPhonesData”Request payload for phone assignment. Uses replace-all semantics — every call replaces all existing assignments.
pub struct AssignPhonesData { pub phone_number_ids: Vec<Uuid>, pub default_phone_number_id: Option<Uuid>,}If default_phone_number_id is None and exactly one phone is assigned, that phone becomes the default automatically.
PhoneAssignmentInfo
Section titled “PhoneAssignmentInfo”Shows a phone number with all members currently assigned to it. Powers the phone picker’s cross-member visibility.
pub struct PhoneAssignmentInfo { pub phone_number_id: Uuid, pub number: String, pub friendly_name: Option<String>, pub assigned_to: Vec<PhoneAssignee>,}
pub struct PhoneAssignee { pub member_id: Uuid, pub member_name: String, pub is_default: bool,}OrgMemberWithRoles
Section titled “OrgMemberWithRoles”Extended to include email and phone assignments:
pub struct OrgMemberWithRoles { pub member_id: Uuid, pub user_id: Uuid, pub name: String, pub email: Option<String>, pub is_owner: bool, pub last_active_at: Option<String>, pub roles: Vec<MemberRoleSummary>, pub phones: Vec<MemberPhoneSummary>,}Database Schema
Section titled “Database Schema”The member_phone_number join table links members to phone numbers:
CREATE TABLE member_phone_number ( id UUID PRIMARY KEY, member_id UUID NOT NULL REFERENCES member(id) ON DELETE CASCADE, phone_number_id UUID NOT NULL REFERENCES phone_number(id) ON DELETE CASCADE, is_default BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(member_id, phone_number_id));Both foreign keys cascade on delete — removing a member clears their phone assignments, and removing a phone number clears all member assignments to it.
API Endpoints
Section titled “API Endpoints”| Method | Path | Description |
|---|---|---|
GET | /api/org/members-with-roles | List members with roles, phones, and emails |
GET | /api/org/phone-assignments | All org phones with their current assignees |
POST | /api/members/{id}/phones | Assign phones to a member (replace-all) |
DELETE | /api/members/{id} | Remove a member from the organization |
PUT | /api/members/{id}/ownership | Promote or demote member ownership |
All endpoints require an authenticated session. Permission checks:
| Endpoint | Permission |
|---|---|
| List members | Member:Collection:List |
| Phone assignments | Member:Collection:List |
| Assign phones | Member:Instance:Update |
| Remove member | Member:Instance:Remove |
| Change ownership | Owner-only (not ABAC-grantable) |
Assign Phones
Section titled “Assign Phones”Send a POST to /api/members/{member_id}/phones:
{ "phone_number_ids": ["uuid-1", "uuid-2"], "default_phone_number_id": "uuid-1"}The server deletes all existing assignments for the member, then batch-inserts the new set. Pass an empty phone_number_ids array to remove all assignments.
Remove Member
Section titled “Remove Member”Send a DELETE to /api/members/{member_id}. The server prevents removal of the organization owner (returns 400). On success, FK cascades remove member_phone_number and member_role records.
Ownership Management
Section titled “Ownership Management”Organizations can have multiple owners. Owners bypass all ABAC permission checks and have full administrative control. Only existing owners can promote or demote other members — ownership is not grantable via roles.
Ownership API
Section titled “Ownership API”PUT /api/members/{member_id}/ownershipRequest:
pub struct TransferOwnershipData { pub is_owner: bool, // true = promote, false = demote}Responses:
| Status | Condition |
|---|---|
200 | Ownership updated |
400 | Last owner guard — cannot remove the only owner |
403 | Caller is not an owner |
404 | Member not found in org |
Server logic:
- Checks
session.is_ownerdirectly (not an ABAC permission) - Validates the target member belongs to the caller’s org
- Returns early if the member’s ownership status already matches the request
- On demotion: counts remaining owners — blocks if only one remains
- Updates
member.is_ownerand logs the change viatracing::info!
ABAC Bypass
Section titled “ABAC Bypass”All owners bypass permission checks. The has_instance_permission and has_collection_permission methods in resource_trait.rs short-circuit when session.is_owner is true:
if session.is_owner { return true; // Bypass all permission checks}This means owners can perform any action on any resource within their organization — view, edit, delete — regardless of their assigned roles.
Last Owner Guard
Section titled “Last Owner Guard”The system prevents removing the last owner to avoid orphaned organizations. When demoting an owner, the server counts all owners in the org:
if !data.is_owner && member.is_owner { let owner_count = count_owners_in_org(); (owner_count > 1).or_bad_request( "Cannot remove the last owner of the organization" )?;}Database
Section titled “Database”The member table includes an is_owner boolean column (DEFAULT false). The migration auto-promotes the earliest member in each organization to owner.
ConfirmOwnershipChange Component
Section titled “ConfirmOwnershipChange Component”The ConfirmOwnershipChange component renders a button and confirmation modal for ownership changes.
#[component]pub fn ConfirmOwnershipChange( member_id: Uuid, member_name: String, is_currently_owner: bool, on_changed: EventHandler,) -> Element- Promote: “Promote to Owner” button → warning modal → calls the ownership API with
is_owner: true - Demote: “Remove Owner” button → destructive modal → calls the ownership API with
is_owner: false
The component only appears in MemberDetailPanel when the caller is an owner (session.is_owner). Members are sorted with owners first in the member list.
UI Components
Section titled “UI Components”MemberList
Section titled “MemberList”The top-level component renders a responsive table (desktop) or card grid (mobile). It loads three resources in parallel:
get_org_members_with_roles_api()— members with roles and phonesget_roles_api()— available roles for the role pickerget_phone_assignments_api()— phone-to-member mapping for the phone picker
Clicking “Manage” on a row opens the MemberDetailPanel inline below the table.
MemberDetailPanel
Section titled “MemberDetailPanel”An inline panel with four sections:
- Phone Numbers — lists assigned phones with badges (filled for default, outline for others). An “Assign Phones” button toggles the
MemberPhonePicker. - Roles — checkbox list of available roles. Each toggle saves immediately via
assign_member_roles_api. - Ownership — promote or demote member ownership via
ConfirmOwnershipChange. Only visible when the caller is an owner. - Danger Zone —
ConfirmRemoveMemberwith two-step confirmation. Hidden for the org owner.
MemberPhonePicker
Section titled “MemberPhonePicker”Checkbox list of all org phones. Each phone row shows:
- Checkbox for assignment
- Phone label (friendly name or number)
- “Assigned to: Alice, Bob” for cross-member visibility
- Star toggle for default designation (visible only when checked)
Every check/uncheck or star click triggers an immediate save. If you uncheck the default phone, the picker auto-selects the first remaining phone as the new default.
ConfirmRemoveMember
Section titled “ConfirmRemoveMember”Two-step inline confirmation: first click shows the prompt, second click executes the deletion. The confirmation box uses destructive styling (border-destructive/30 bg-destructive/5).
Query Patterns
Section titled “Query Patterns”All APIs use batch loading to avoid N+1 queries:
// Collect all member IDslet member_ids: Vec<Uuid> = members.iter().map(|(m, _)| m.id).collect();
// Single batch query for phone assignmentslet member_phones = schemas::member_phone_number::Entity::find() .filter(schemas::member_phone_number::Column::MemberId.is_in(member_ids)) .all(&db) .await?;The get_org_members_with_roles_api endpoint runs a fixed ~6 queries regardless of member count.
Module Structure
Section titled “Module Structure”src/mods/settings/├── api/│ ├── get_org_members_with_roles_api.rs│ ├── get_phone_assignments_api.rs│ ├── assign_member_phones_api.rs│ ├── remove_member_api.rs│ └── transfer_ownership_api.rs├── types/│ ├── member_phone_type.rs│ ├── phone_assignment_info_type.rs│ └── org_member_with_roles_type.rs # includes TransferOwnershipData└── components/ ├── member_list_component.rs ├── member_detail_panel_component.rs ├── member_phone_picker_component.rs ├── confirm_remove_member_component.rs └── confirm_ownership_change_component.rsRelated Pages
Section titled “Related Pages”- Settings — parent module with account, telephony, and integration config
- Auth: Role Management — role CRUD and permission model
- Phone Numbers — phone number import and management