2026-01-12 05:03:09 +00:00

766 lines
31 KiB
Rust

use leptos::prelude::*;
use leptos::task::spawn_local;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: String,
pub email: String,
pub name: String,
pub status: UserStatus,
pub roles: Vec<String>,
pub groups: Vec<String>,
pub created_at: String,
pub last_login: Option<String>,
pub mfa_enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum UserStatus {
Active,
Inactive,
Suspended,
PendingVerification,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Role {
pub id: String,
pub name: String,
pub description: String,
pub permissions: Vec<String>,
pub parent_role: Option<String>,
pub children: Vec<String>,
pub level: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Group {
pub id: String,
pub name: String,
pub description: String,
pub members: Vec<String>,
pub parent_group: Option<String>,
pub children: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Permission {
pub id: String,
pub name: String,
pub description: String,
pub resource: String,
pub action: String,
pub category: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AccessReviewCampaign {
pub id: String,
pub name: String,
pub description: String,
pub schedule: String,
pub reviewers: Vec<String>,
pub status: String,
pub due_date: String,
}
#[component]
pub fn UsersPage() -> impl IntoView {
let (active_tab, set_active_tab) = signal("users".to_string());
let (users, set_users) = signal(Vec::<User>::new());
let (roles, set_roles) = signal(Vec::<Role>::new());
let (groups, set_groups) = signal(Vec::<Group>::new());
let (permissions, set_permissions) = signal(Vec::<Permission>::new());
let (selected_user, set_selected_user) = signal(None::<User>);
let (show_user_modal, set_show_user_modal) = signal(false);
let (show_role_hierarchy, set_show_role_hierarchy) = signal(false);
let (show_bulk_operations, set_show_bulk_operations) = signal(false);
// Load initial data
Effect::new(move |_| {
spawn_local(async move {
// Mock data - in real app, fetch from API
let mock_users = vec![
User {
id: "u1".to_string(),
email: "admin@example.com".to_string(),
name: "System Admin".to_string(),
status: UserStatus::Active,
roles: vec!["admin".to_string()],
groups: vec!["admins".to_string()],
created_at: "2024-01-01".to_string(),
last_login: Some("2024-01-15".to_string()),
mfa_enabled: true,
},
User {
id: "u2".to_string(),
email: "user@example.com".to_string(),
name: "Regular User".to_string(),
status: UserStatus::Active,
roles: vec!["user".to_string()],
groups: vec!["users".to_string()],
created_at: "2024-01-10".to_string(),
last_login: Some("2024-01-14".to_string()),
mfa_enabled: false,
},
];
let mock_roles = vec![
Role {
id: "admin".to_string(),
name: "Administrator".to_string(),
description: "Full system access".to_string(),
permissions: vec!["*".to_string()],
parent_role: None,
children: vec!["operator".to_string()],
level: 0,
},
Role {
id: "operator".to_string(),
name: "Operator".to_string(),
description: "Infrastructure management".to_string(),
permissions: vec!["infra:*".to_string()],
parent_role: Some("admin".to_string()),
children: vec!["user".to_string()],
level: 1,
},
Role {
id: "user".to_string(),
name: "User".to_string(),
description: "Basic access".to_string(),
permissions: vec!["read:*".to_string()],
parent_role: Some("operator".to_string()),
children: vec![],
level: 2,
},
];
set_users.set(mock_users);
set_roles.set(mock_roles);
});
});
view! {
<div class="users-page h-full flex flex-col">
// Header
<div class="flex-none border-b border-base-300 p-4">
<div class="flex items-center justify-between">
<h1 class="text-3xl font-bold">"User Management"</h1>
<div class="flex gap-2">
<button
class="btn btn-outline btn-sm"
on:click=move |_| set_show_bulk_operations.set(true)
>
"Bulk Operations"
</button>
<button
class="btn btn-outline btn-sm"
on:click=move |_| set_show_role_hierarchy.set(true)
>
"Role Hierarchy"
</button>
<button
class="btn btn-primary btn-sm"
on:click=move |_| {
set_selected_user.set(None);
set_show_user_modal.set(true);
}
>
"Add User"
</button>
</div>
</div>
// Tab Navigation
<div class="tabs tabs-boxed mt-4">
<a
class={move || if active_tab.get() == "users" { "tab tab-active" } else { "tab" }}
on:click=move |_| set_active_tab.set("users".to_string())
>
"Users"
</a>
<a
class={move || if active_tab.get() == "roles" { "tab tab-active" } else { "tab" }}
on:click=move |_| set_active_tab.set("roles".to_string())
>
"Roles"
</a>
<a
class={move || if active_tab.get() == "groups" { "tab tab-active" } else { "tab" }}
on:click=move |_| set_active_tab.set("groups".to_string())
>
"Groups"
</a>
<a
class={move || if active_tab.get() == "permissions" { "tab tab-active" } else { "tab" }}
on:click=move |_| set_active_tab.set("permissions".to_string())
>
"Permissions"
</a>
<a
class={move || if active_tab.get() == "reviews" { "tab tab-active" } else { "tab" }}
on:click=move |_| set_active_tab.set("reviews".to_string())
>
"Access Reviews"
</a>
</div>
</div>
// Content Area
<div class="flex-1 overflow-hidden">
{move || match active_tab.get().as_str() {
"users" => view! { <UsersTab users=users set_selected_user=set_selected_user set_show_user_modal=set_show_user_modal /> }.into(),
"roles" => view! { <RolesTab roles=roles /> }.into(),
"groups" => view! { <GroupsTab groups=groups /> }.into(),
"permissions" => view! { <PermissionsTab permissions=permissions /> }.into(),
"reviews" => view! { <AccessReviewsTab /> }.into(),
_ => view! { <div>"Unknown tab"</div> }.into(),
}}
</div>
// User Modal
{move || if show_user_modal.get() {
view! {
<UserModal
user=selected_user
show=show_user_modal
set_show=set_show_user_modal
roles=roles.get()
groups=groups.get()
/>
}.into()
} else {
view! { <div></div> }.into()
}}
// Role Hierarchy Modal
{move || if show_role_hierarchy.get() {
view! {
<RoleHierarchyModal
roles=roles.get()
show=show_role_hierarchy
set_show=set_show_role_hierarchy
/>
}.into()
} else {
view! { <div></div> }.into()
}}
// Bulk Operations Modal
{move || if show_bulk_operations.get() {
view! {
<BulkOperationsModal
show=show_bulk_operations
set_show=set_show_bulk_operations
/>
}.into()
} else {
view! { <div></div> }.into()
}}
</div>
}
}
#[component]
fn UsersTab(
users: ReadSignal<Vec<User>>,
set_selected_user: WriteSignal<Option<User>>,
set_show_user_modal: WriteSignal<bool>,
) -> impl IntoView {
let (search_term, set_search_term) = signal(String::new());
let (status_filter, set_status_filter) = signal("all".to_string());
let filtered_users = Memo::new(move |_| {
let search = search_term.get().to_lowercase();
let status = status_filter.get();
users.get().into_iter().filter(|user| {
let matches_search = search.is_empty() ||
user.name.to_lowercase().contains(&search) ||
user.email.to_lowercase().contains(&search);
let matches_status = status == "all" ||
format!("{:?}", user.status).to_lowercase() == status;
matches_search && matches_status
}).collect::<Vec<_>>()
});
view! {
<div class="p-4 h-full flex flex-col">
// Search and Filters
<div class="flex-none mb-4 flex gap-4 items-center">
<div class="flex-1">
<input
type="text"
placeholder="Search users..."
class="input input-bordered w-full"
prop:value=move || search_term.get()
on:input=move |ev| set_search_term.set(event_target_value(&ev))
/>
</div>
<select
class="select select-bordered"
on:change=move |ev| set_status_filter.set(event_target_value(&ev))
>
<option value="all">"All Status"</option>
<option value="active">"Active"</option>
<option value="inactive">"Inactive"</option>
<option value="suspended">"Suspended"</option>
<option value="pendingverification">"Pending"</option>
</select>
</div>
// Users Table
<div class="flex-1 overflow-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>"Name"</th>
<th>"Email"</th>
<th>"Status"</th>
<th>"Roles"</th>
<th>"MFA"</th>
<th>"Last Login"</th>
<th>"Actions"</th>
</tr>
</thead>
<tbody>
{move || {
filtered_users.get().into_iter().map(|user| {
let user_clone = user.clone();
let user_clone2 = user.clone();
view! {
<tr>
<td class="font-medium">{&user.name}</td>
<td>{&user.email}</td>
<td>
<div class={format!("badge {}",
match user.status {
UserStatus::Active => "badge-success",
UserStatus::Inactive => "badge-warning",
UserStatus::Suspended => "badge-error",
UserStatus::PendingVerification => "badge-info",
}
)}>
{format!("{:?}", user.status)}
</div>
</td>
<td>
<div class="flex gap-1 flex-wrap">
{user.roles.into_iter().map(|role| {
view! {
<div class="badge badge-outline badge-sm">{role}</div>
}
}).collect::<Vec<_>>()}
</div>
</td>
<td>
{if user.mfa_enabled {
view! { <div class="badge badge-success">"Enabled"</div> }
} else {
view! { <div class="badge badge-warning">"Disabled"</div> }
}}
</td>
<td class="text-sm text-base-content/70">
{user.last_login.unwrap_or_else(|| "Never".to_string())}
</td>
<td>
<div class="flex gap-2">
<button
class="btn btn-ghost btn-xs"
on:click=move |_| {
set_selected_user.set(Some(user_clone.clone()));
set_show_user_modal.set(true);
}
>
"Edit"
</button>
<button class="btn btn-ghost btn-xs text-error">
"Delete"
</button>
</div>
</td>
</tr>
}
}).collect::<Vec<_>>()
}}
</tbody>
</table>
</div>
</div>
}
}
#[component]
fn RolesTab(roles: ReadSignal<Vec<Role>>) -> impl IntoView {
view! {
<div class="p-4 h-full">
<div class="mb-4">
<button class="btn btn-primary btn-sm">"Add Role"</button>
</div>
<div class="grid gap-4">
{move || {
roles.get().into_iter().map(|role| {
view! {
<div class="card bg-base-200 p-4">
<div class="flex items-start justify-between">
<div>
<h3 class="font-semibold">{&role.name}</h3>
<p class="text-sm text-base-content/70 mt-1">{&role.description}</p>
<div class="mt-2">
<span class="text-xs font-medium">"Permissions: "</span>
<div class="flex gap-1 flex-wrap mt-1">
{role.permissions.into_iter().map(|perm| {
view! {
<div class="badge badge-outline badge-xs">{perm}</div>
}
}).collect::<Vec<_>>()}
</div>
</div>
{role.parent_role.as_ref().map(|parent| {
view! {
<div class="mt-2 text-xs">
"Inherits from: "
<span class="badge badge-ghost badge-xs">{parent}</span>
</div>
}
})}
</div>
<div class="flex gap-2">
<button class="btn btn-ghost btn-xs">"Edit"</button>
<button class="btn btn-ghost btn-xs text-error">"Delete"</button>
</div>
</div>
</div>
}
}).collect::<Vec<_>>()
}}
</div>
</div>
}
}
#[component]
fn GroupsTab(groups: ReadSignal<Vec<Group>>) -> impl IntoView {
view! {
<div class="p-4">
<div class="text-center text-base-content/50 mt-8">
"Group management coming soon..."
</div>
</div>
}
}
#[component]
fn PermissionsTab(permissions: ReadSignal<Vec<Permission>>) -> impl IntoView {
view! {
<div class="p-4">
<PermissionMatrix />
</div>
}
}
#[component]
fn AccessReviewsTab() -> impl IntoView {
view! {
<div class="p-4">
<div class="mb-4">
<button class="btn btn-primary btn-sm">"Create Campaign"</button>
</div>
<div class="text-center text-base-content/50 mt-8">
"Access review campaigns coming soon..."
</div>
</div>
}
}
#[component]
fn UserModal(
user: ReadSignal<Option<User>>,
show: ReadSignal<bool>,
set_show: WriteSignal<bool>,
roles: Vec<Role>,
groups: Vec<Group>,
) -> impl IntoView {
let (name, set_name) = signal(String::new());
let (email, set_email) = signal(String::new());
let (selected_roles, set_selected_roles) = signal(Vec::<String>::new());
Effect::new(move |_| {
if let Some(u) = user.get() {
set_name.set(u.name);
set_email.set(u.email);
set_selected_roles.set(u.roles);
} else {
set_name.set(String::new());
set_email.set(String::new());
set_selected_roles.set(Vec::new());
}
});
view! {
<div class={move || if show.get() { "modal modal-open" } else { "modal" }}>
<div class="modal-box w-11/12 max-w-2xl">
<h3 class="font-bold text-lg">
{move || if user.get().is_some() { "Edit User" } else { "Add User" }}
</h3>
<div class="py-4 space-y-4">
<div>
<label class="label">"Name"</label>
<input
type="text"
class="input input-bordered w-full"
prop:value=move || name.get()
on:input=move |ev| set_name.set(event_target_value(&ev))
/>
</div>
<div>
<label class="label">"Email"</label>
<input
type="email"
class="input input-bordered w-full"
prop:value=move || email.get()
on:input=move |ev| set_email.set(event_target_value(&ev))
/>
</div>
<div>
<label class="label">"Roles"</label>
<div class="space-y-2">
{roles.into_iter().map(|role| {
let role_id = role.id.clone();
view! {
<label class="label cursor-pointer justify-start gap-2">
<input
type="checkbox"
class="checkbox"
prop:checked=move || selected_roles.get().contains(&role_id)
on:change=move |_| {
let mut current = selected_roles.get();
if current.contains(&role_id) {
current.retain(|r| r != &role_id);
} else {
current.push(role_id.clone());
}
set_selected_roles.set(current);
}
/>
<span class="label-text">{&role.name}</span>
<span class="text-xs text-base-content/50">{"("}{&role.description}{")"}</span>
</label>
}
}).collect::<Vec<_>>()}
</div>
</div>
</div>
<div class="modal-action">
<button
class="btn"
on:click=move |_| set_show.set(false)
>
"Cancel"
</button>
<button class="btn btn-primary">"Save"</button>
</div>
</div>
</div>
}
}
#[component]
fn RoleHierarchyModal(
roles: Vec<Role>,
show: ReadSignal<bool>,
set_show: WriteSignal<bool>,
) -> impl IntoView {
view! {
<div class={move || if show.get() { "modal modal-open" } else { "modal" }}>
<div class="modal-box w-11/12 max-w-4xl">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg">"Role Hierarchy"</h3>
<button
class="btn btn-ghost btn-sm"
on:click=move |_| set_show.set(false)
>
""
</button>
</div>
<div class="py-4">
<RoleHierarchyViewer roles=roles />
</div>
</div>
</div>
}
}
#[component]
fn RoleHierarchyViewer(roles: Vec<Role>) -> impl IntoView {
// Sort roles by level for hierarchy display
let mut sorted_roles = roles;
sorted_roles.sort_by_key(|r| r.level);
view! {
<div class="space-y-4">
{sorted_roles.into_iter().map(|role| {
let indent = role.level * 24; // 24px per level
view! {
<div class="flex items-center" style={format!("margin-left: {}px", indent)}>
<div class="w-4 h-4 border-2 border-primary rounded mr-3"></div>
<div class="flex-1">
<div class="font-medium">{&role.name}</div>
<div class="text-sm text-base-content/70">{&role.description}</div>
<div class="flex gap-1 mt-1">
{role.permissions.into_iter().take(3).map(|perm| {
view! {
<div class="badge badge-outline badge-xs">{perm}</div>
}
}).collect::<Vec<_>>()}
{if role.permissions.len() > 3 {
view! {
<div class="badge badge-ghost badge-xs">
{"+"}{role.permissions.len() - 3}{" more"}
</div>
}.into()
} else {
view! { <div></div> }.into()
}}
</div>
</div>
<div class="flex gap-2">
<button class="btn btn-ghost btn-xs">"Edit"</button>
</div>
</div>
}
}).collect::<Vec<_>>()}
</div>
}
}
#[component]
fn PermissionMatrix() -> impl IntoView {
let resources = vec!["Users", "Roles", "Infrastructure", "Workflows", "Policies"];
let actions = vec!["Read", "Write", "Delete", "Execute", "Admin"];
let roles = vec!["Admin", "Operator", "User"];
view! {
<div class="overflow-auto">
<table class="table table-zebra table-compact w-full">
<thead>
<tr>
<th>"Resource"</th>
<th>"Action"</th>
{roles.iter().map(|role| {
let role = role.clone();
view! { <th>{role}</th> }
}).collect::<Vec<_>>()}
</tr>
</thead>
<tbody>
{resources.into_iter().flat_map(|resource| {
actions.iter().map({
let resource = resource.clone();
move |action| {
view! {
<tr>
<td>{&resource}</td>
<td>{action}</td>
{roles.iter().map(|role| {
let role = role.clone();
view! {
<td>
<input type="checkbox" class="checkbox checkbox-xs" />
</td>
}
}).collect::<Vec<_>>()}
</tr>
}
}
}).collect::<Vec<_>>()
}).collect::<Vec<_>>()}
</tbody>
</table>
</div>
}
}
#[component]
fn BulkOperationsModal(
show: ReadSignal<bool>,
set_show: WriteSignal<bool>,
) -> impl IntoView {
let (operation_type, set_operation_type) = signal("import".to_string());
view! {
<div class={move || if show.get() { "modal modal-open" } else { "modal" }}>
<div class="modal-box">
<div class="flex justify-between items-center mb-4">
<h3 class="font-bold text-lg">"Bulk Operations"</h3>
<button
class="btn btn-ghost btn-sm"
on:click=move |_| set_show.set(false)
>
""
</button>
</div>
<div class="py-4 space-y-4">
<div>
<label class="label">"Operation Type"</label>
<select
class="select select-bordered w-full"
on:change=move |ev| set_operation_type.set(event_target_value(&ev))
>
<option value="import">"Import Users"</option>
<option value="export">"Export Users"</option>
<option value="bulk_update">"Bulk Update"</option>
<option value="bulk_delete">"Bulk Delete"</option>
</select>
</div>
{move || match operation_type.get().as_str() {
"import" => view! {
<div>
<label class="label">"CSV File"</label>
<input type="file" class="file-input file-input-bordered w-full" accept=".csv" />
<div class="text-sm text-base-content/70 mt-2">
"Expected format: name,email,roles (comma-separated)"
</div>
</div>
}.into(),
"export" => view! {
<div>
<label class="label cursor-pointer">
<input type="checkbox" class="checkbox" />
<span class="label-text">"Include sensitive data"</span>
</label>
<label class="label cursor-pointer">
<input type="checkbox" class="checkbox" />
<span class="label-text">"Include inactive users"</span>
</label>
</div>
}.into(),
_ => view! { <div>"Operation settings..."</div> }.into(),
}}
</div>
<div class="modal-action">
<button
class="btn"
on:click=move |_| set_show.set(false)
>
"Cancel"
</button>
<button class="btn btn-primary">"Execute"</button>
</div>
</div>
</div>
}
}