Admin & Property Manager Roles
This document covers administrative roles, portfolio management, and Property Manager (PM) specific functionality.
Key Files
crates/bf_resident/src/handlers/portfolios.rs- Portfolio CRUD operationscrates/bf_resident/src/handlers/pm_filtered.rs- PM-scoped handlerscrates/clerk-auth/src/roles.rs- Role definitions and checkscrates/bf_user/src/permissions.rs- DO-level permission enforcementcrates/bf_resident_db/src/operations/portfolios.rs- Portfolio database operations
Role Hierarchy
┌─────────────────────────────────────────────────────────────────────┐
│ Role Hierarchy │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Admin ─────────────────────────────────────────────────────────────│
│ │ • Full system access │
│ │ • All portfolios and properties │
│ │ • User management (via Clerk) │
│ │ │
│ ├── ItAdmin ─────────────────────────────────────────────────────│
│ │ │ • Device commissioning │
│ │ │ • SmartThings/Yale/Hive setup │
│ │ │ • Credential management │
│ │ │ │
│ │ └── PropertyManager ───────────────────────────────────────│
│ │ │ • Portfolio-scoped access │
│ │ │ • Tenant management │
│ │ │ • PM PIN codes (vacant properties) │
│ │ │ • Property lifecycle transitions │
│ │ │ │
│ │ └── Installer ───────────────────────────────────────│
│ │ │ • Device installation │
│ │ │ • Commissioning tasks │
│ │ │ • Portfolio-assigned access │
│ │ │ │
│ │ └── Tenant ────────────────────────────────────│
│ │ │ • Property access │
│ │ │ • Guest management │
│ │ │ • Device control │
│ │ │ │
│ │ └── Guest ───────────────────────────────│
│ │ • Limited device access │
│ │ • Time-restricted access │
│ │ • Granular permissions │
│ │ │
└─────────────────────────────────────────────────────────────────────┘
Portfolio Model
Portfolios group properties under common management. They define PM assignments and default installers.
Portfolio Structure
pub struct Portfolio {
pub id: String,
pub name: String,
pub description: Option<String>,
pub property_count: u32,
/// PMs assigned to this portfolio (email addresses)
pub assigned_pms: Vec<String>,
/// Default installers for new properties
pub default_installers: Option<Vec<String>>,
/// Housebuilder/developer reference
pub housebuilder: Option<String>,
/// Site map configuration (for installer app)
pub site_map_data: Option<InstallerSiteMapData>,
pub created_at: i64,
pub created_by: String,
pub last_activity: i64,
}
Portfolio Operations
| Operation | Endpoint | Role Required |
|---|---|---|
| Create Portfolio | POST /admin/portfolios | Admin |
| Get Portfolio | GET /portfolios/{id} | PM (if assigned) or Admin |
| Update Portfolio | PUT /admin/portfolios/{id} | Admin |
| Delete Portfolio | DELETE /admin/portfolios/{id} | Admin |
| Add PM | POST /admin/portfolios/{id}/property-managers/{user_id} | Admin |
| Remove PM | DELETE /admin/portfolios/{id}/property-managers/{user_id} | Admin |
| List Properties | GET /portfolios/{id}/properties | PM (if assigned) or Admin |
Portfolio Deletion Rules
Portfolios can only be deleted if they have no properties:
// crates/bf_resident/src/handlers/portfolios.rs:129
let count_result = sqlx_d1::query!(
"SELECT COUNT(*) as count FROM properties WHERE portfolio_id = ?",
id
)
.fetch_one(&mut conn)
.await?;
if count > 0 {
return Err(AppError::BadRequest(
"Cannot delete portfolio with existing properties".to_string(),
));
}
PM-Filtered Access
Property Managers have access filtered to their assigned portfolios.
Automatic Filtering
// crates/bf_resident/src/handlers/pm_filtered.rs:42
pub async fn list_portfolios_pm(
Extension(auth): Extension<ClerkAuth>,
State(env): State<Env>,
Query(mut filters): Query<PortfolioFilters>,
) -> Result<Json<PortfolioListResponse>> {
let user_id = auth.user_id().to_string();
// If not admin, force filter by assigned PM
if !is_admin(&auth) {
filters.assigned_pm = Some(user_id);
}
// ...
}
Portfolio Assignment Check
// crates/bf_resident/src/handlers/pm_filtered.rs:85
pub async fn get_portfolio_id_by_name(
Extension(auth): Extension<ClerkAuth>,
State(env): State<Env>,
Path(name): Path<String>,
) -> Result<Json<IdResponse>> {
let portfolio = get_portfolio_by_name(&mut conn, &name.to_lowercase()).await?;
// Check if PM has access (admins can access all)
if !is_admin(&auth) {
let user_id = auth.user_id().to_string();
if !portfolio.assigned_pms.contains(&user_id) {
return Err(AppError::Forbidden(
"Not assigned to this portfolio".to_string(),
));
}
}
// ...
}
PM PIN Code Management
PMs can create temporary PIN codes for vacant properties (Commissioned or PreTenancy status).
Property Access Verification
// crates/bf_resident/src/handlers/pm_filtered.rs:138
pub async fn verify_pm_property_access(
conn: &mut D1Connection,
auth: &ClerkAuth,
property_id: &str,
) -> Result<Property> {
let property = get_property_by_id(conn, property_id).await?;
// Check PM has access to portfolio
if !is_admin(auth) {
let portfolio = get_portfolio_by_id(conn, &property.portfolio_id).await?;
if !portfolio.assigned_pms.contains(&auth.user_id()) {
return Err(AppError::Forbidden("Not assigned to this portfolio"));
}
}
// Verify property status is Commissioned or PreTenancy
if !matches!(
property.status,
PropertyStatus::Commissioned | PropertyStatus::PreTenancy
) {
return Err(AppError::BadRequest(
"PIN code management only available for Commissioned or Pre-Tenancy properties"
));
}
Ok(property)
}
PM PIN Code Types
Two metadata types define PM access scenarios:
pub enum PmAccessMetadata {
/// Single-day viewing with time window
TenantViewing {
date: String, // "YYYY-MM-DD"
start_time: String, // "HH:MM"
end_time: String, // "HH:MM"
tenant_name: Option<String>,
tenant_email: Option<String>,
tenant_phone: Option<String>,
additional_details: Option<String>,
},
/// Multi-day contractor access
ContractorVisit {
contractor_type: ContractorType,
date_start: String, // "YYYY-MM-DD"
date_end: String, // "YYYY-MM-DD"
start_time: String, // Daily "HH:MM"
end_time: String, // Daily "HH:MM"
additional_details: Option<String>,
},
}
PIN Code Constraints
| Constraint | Tenant Viewing | Contractor Visit |
|---|---|---|
| Duration | Max 10 hours | Max 14 days |
| Time Window | Single window | Daily recurring |
| Status Required | Commissioned/PreTenancy | Commissioned/PreTenancy |
/// Maximum days for PM-created PIN codes
pub const PM_PIN_CODE_MAX_DAYS: i64 = 14;
// Validate viewing duration (max 10 hours = 600 minutes)
let duration_minutes = end_minutes - start_minutes;
if duration_minutes > 600 {
return Err(AppError::BadRequest(
"PIN code duration cannot exceed 10 hours"
));
}
PM Security Controls
PMs can monitor and control security for vacant properties.
Security Status
// GET /pm/properties/{property_id}/security-status
pub struct SecurityStatusResponse {
pub locks: Vec<LockStatus>,
pub alarm: Option<AlarmStatus>,
pub issues: Vec<SecurityIssue>,
pub last_updated: i64,
}
Available Actions
| Action | Endpoint | Description |
|---|---|---|
| Lock Door | POST /pm/properties/{id}/lock | Remote lock |
| Arm Alarm | POST /pm/properties/{id}/arm-alarm | Set to Away mode |
| Get Status | GET /pm/properties/{id}/security-status | Current state |
Security Reminder Emails
When a PM creates a PIN code, they receive an email reminder:
// Email includes:
// - Property address
// - Access type and schedule
// - Security reminder checklist:
// - Check door is locked
// - Check alarm is armed
// - Verify all secure
Activity Logging
All PM actions are logged for audit purposes:
let activity = CreateActivityLogRequest {
property_id: Some(property_id.clone()),
portfolio_id: Some(property.portfolio_id.clone()),
user_email: pm_email.clone(),
action: "pm_access_code_created".to_string(),
description: format!(
"PM created access code '{}': {}",
request.name.trim(),
details
),
metadata: Some(serde_json::json!({
"pin_code_id": code.id,
"pin_code_name": code.name,
"access_type": access_type,
"valid_from": valid_from,
"valid_until": valid_until,
})),
};
Logged Actions
pm_access_code_created- PIN code created by PMpm_access_code_revoked- PIN code revoked by PMpm_added- PM added to portfoliopm_removed- PM removed from portfolioportfolio_updated- Portfolio configuration changed- Property status transitions
Slug-Based Routing
PMs can access resources via human-readable slugs:
// Get portfolio ID by name
// GET /portfolios/by-name/{name}
pub async fn get_portfolio_id_by_name(name: Path<String>) -> Json<IdResponse>
// Get property ID by portfolio name and address
// GET /portfolios/by-name/{portfolio_name}/properties/by-address/{address}
pub async fn get_property_id_by_address(
(portfolio_name, address): Path<(String, String)>
) -> Json<IdResponse>
Admin-Only Operations
Operations restricted to Admin role:
| Operation | Description |
|---|---|
| Portfolio CRUD | Create, update, delete portfolios |
| PM Assignment | Add/remove PMs from portfolios |
| Bulk Property Creation | Create multiple properties at once |
| User Invites | Send Clerk invitations |
| System Configuration | Environment settings |
Installer Role
Installers are assigned to portfolios via default_installers:
pub struct Portfolio {
// ...
/// Default installers auto-assigned to new properties
pub default_installers: Option<Vec<String>>,
}
Installer Capabilities
- View assigned properties in commissioning status
- Complete device installation tasks
- Access site maps for property location
- Mark commissioning steps complete
Site Map Access
Installers can view portfolio site maps:
// GET /installer/portfolios/{portfolio_id}/site-map
pub struct InstallerSiteMapData {
pub image_url: String, // Presigned URL (1 hour validity)
pub image_id: String,
pub lat: f64,
pub lng: f64,
pub opacity: f64,
pub bounds: Option<SiteMapBounds>,
}
Role Checking Functions
// crates/clerk-auth/src/roles.rs
pub fn is_admin(auth: &ClerkAuth) -> bool {
auth.roles().contains(&Role::Admin)
}
pub fn is_it_admin(auth: &ClerkAuth) -> bool {
is_admin(auth) || auth.roles().contains(&Role::ItAdmin)
}
pub fn is_property_manager(auth: &ClerkAuth) -> bool {
is_it_admin(auth) || auth.roles().contains(&Role::PropertyManager)
}
pub fn is_installer(auth: &ClerkAuth) -> bool {
is_property_manager(auth) || auth.roles().contains(&Role::Installer)
}
See Also
- Permissions - RBAC and DO permissions
- Property Lifecycle - Status transitions
- Move-In & Move-Out - Tenant transitions
- Authentication - Clerk JWT auth