Some checks failed
BotServer CI / build (push) Failing after 1m34s
Split 20+ files over 1000 lines into focused subdirectories for better maintainability and code organization. All changes maintain backward compatibility through re-export wrappers. Major splits: - attendance/llm_assist.rs (2074→7 modules) - basic/keywords/face_api.rs → face_api/ (7 modules) - basic/keywords/file_operations.rs → file_ops/ (8 modules) - basic/keywords/hear_talk.rs → hearing/ (6 modules) - channels/wechat.rs → wechat/ (10 modules) - channels/youtube.rs → youtube/ (5 modules) - contacts/mod.rs → contacts_api/ (6 modules) - core/bootstrap/mod.rs → bootstrap/ (5 modules) - core/shared/admin.rs → admin_*.rs (5 modules) - designer/canvas.rs → canvas_api/ (6 modules) - designer/mod.rs → designer_api/ (6 modules) - docs/handlers.rs → handlers_api/ (11 modules) - drive/mod.rs → drive_handlers.rs, drive_types.rs - learn/mod.rs → types.rs - main.rs → main_module/ (7 modules) - meet/webinar.rs → webinar_api/ (8 modules) - paper/mod.rs → (10 modules) - security/auth.rs → auth_api/ (7 modules) - security/passkey.rs → (4 modules) - sources/mod.rs → sources_api/ (5 modules) - tasks/mod.rs → task_api/ (5 modules) Stats: 38,040 deletions, 1,315 additions across 318 files Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
637 lines
29 KiB
Rust
637 lines
29 KiB
Rust
use crate::security::error_sanitizer::SafeErrorResponse;
|
|
use crate::core::shared::models::{RbacGroup, RbacRole, User};
|
|
use crate::core::shared::state::AppState;
|
|
use axum::{
|
|
extract::{Path, State},
|
|
response::{Html, IntoResponse},
|
|
};
|
|
use diesel::prelude::*;
|
|
use std::sync::Arc;
|
|
use uuid::Uuid;
|
|
|
|
pub async fn rbac_settings_page() -> Html<String> {
|
|
Html(String::from(r##"<div class="rbac-settings">
|
|
<h2>Access Control</h2>
|
|
<p class="text-muted">Manage users, roles, and groups using Role-Based Access Control (RBAC)</p>
|
|
|
|
<div class="rbac-tabs">
|
|
<button class="tab-btn active" hx-get="/settings/rbac/users" hx-target="#rbac-content" hx-swap="innerHTML">
|
|
<span class="tab-icon">👥</span> Users
|
|
</button>
|
|
<button class="tab-btn" hx-get="/settings/rbac/roles" hx-target="#rbac-content" hx-swap="innerHTML">
|
|
<span class="tab-icon">🔐</span> Roles
|
|
</button>
|
|
<button class="tab-btn" hx-get="/settings/rbac/groups" hx-target="#rbac-content" hx-swap="innerHTML">
|
|
<span class="tab-icon">📁</span> Groups
|
|
</button>
|
|
</div>
|
|
|
|
<div id="rbac-content" hx-get="/settings/rbac/users" hx-trigger="load" hx-swap="innerHTML">
|
|
<div class="loading">Loading...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.rbac-settings { padding: 1rem; }
|
|
.rbac-tabs { display: flex; gap: 0.5rem; margin-bottom: 1rem; border-bottom: 1px solid #e0e0e0; padding-bottom: 0.5rem; }
|
|
.tab-btn { background: none; border: none; padding: 0.75rem 1rem; cursor: pointer; border-radius: 4px 4px 0 0; transition: all 0.2s; }
|
|
.tab-btn:hover { background: #f5f5f5; }
|
|
.tab-btn.active { background: #e3f2fd; color: #1976d2; font-weight: 500; }
|
|
.tab-icon { margin-right: 0.5rem; }
|
|
|
|
.user-list, .role-list, .group-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
.user-item, .role-item, .group-item { display: flex; align-items: center; justify-content: space-between; padding: 0.75rem 1rem; background: #f9f9f9; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
|
|
.user-item:hover, .role-item:hover, .group-item:hover { background: #e3f2fd; }
|
|
.user-item.selected { background: #bbdefb; border-left: 3px solid #1976d2; }
|
|
|
|
.assignment-panel { display: grid; grid-template-columns: 1fr auto 1fr; gap: 1rem; margin-top: 1rem; padding: 1rem; background: #fafafa; border-radius: 8px; }
|
|
.list-box { background: white; border: 1px solid #e0e0e0; border-radius: 8px; min-height: 300px; max-height: 400px; overflow-y: auto; }
|
|
.list-box-header { padding: 0.75rem 1rem; background: #f5f5f5; border-bottom: 1px solid #e0e0e0; font-weight: 500; }
|
|
.list-box-content { padding: 0.5rem; }
|
|
.list-item { padding: 0.5rem 0.75rem; border-radius: 4px; cursor: pointer; display: flex; align-items: center; justify-content: space-between; }
|
|
.list-item:hover { background: #e3f2fd; }
|
|
.list-item.selected { background: #bbdefb; }
|
|
|
|
.transfer-buttons { display: flex; flex-direction: column; justify-content: center; gap: 0.5rem; }
|
|
.transfer-btn { padding: 0.5rem 1rem; border: 1px solid #1976d2; background: white; color: #1976d2; border-radius: 4px; cursor: pointer; font-size: 1.2rem; }
|
|
.transfer-btn:hover { background: #1976d2; color: white; }
|
|
.transfer-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
.search-box { padding: 0.5rem; border-bottom: 1px solid #e0e0e0; }
|
|
.search-box input { width: 100%; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; }
|
|
|
|
.badge { padding: 0.25rem 0.5rem; border-radius: 12px; font-size: 0.75rem; }
|
|
.badge-system { background: #e8f5e9; color: #2e7d32; }
|
|
.badge-custom { background: #fff3e0; color: #ef6c00; }
|
|
.badge-role { background: #e3f2fd; color: #1565c0; }
|
|
.badge-group { background: #f3e5f5; color: #7b1fa2; }
|
|
|
|
.empty-state { text-align: center; padding: 2rem; color: #666; }
|
|
.loading { text-align: center; padding: 2rem; color: #666; }
|
|
|
|
.btn-primary { background: #1976d2; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
|
|
.btn-primary:hover { background: #1565c0; }
|
|
.btn-secondary { background: white; color: #1976d2; border: 1px solid #1976d2; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
|
|
.btn-secondary:hover { background: #e3f2fd; }
|
|
.btn-danger { background: #d32f2f; color: white; border: none; padding: 0.25rem 0.5rem; border-radius: 4px; cursor: pointer; font-size: 0.8rem; }
|
|
.btn-danger:hover { background: #c62828; }
|
|
|
|
.create-form { display: flex; gap: 0.5rem; margin-bottom: 1rem; padding: 1rem; background: #f5f5f5; border-radius: 8px; }
|
|
.create-form input { flex: 1; padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; }
|
|
</style>"##))
|
|
}
|
|
|
|
fn render_error(context: &str) -> String {
|
|
tracing::warn!(context = %context, "Error occurred in RBAC UI");
|
|
let response = SafeErrorResponse::internal_error();
|
|
format!(r##"<div class="error">{}</div>"##, response.message)
|
|
}
|
|
|
|
pub async fn rbac_users_list(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
|
let conn = state.conn.clone();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
let mut db_conn = conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
use crate::core::shared::models::schema::users;
|
|
users::table
|
|
.filter(users::is_active.eq(true))
|
|
.order(users::username.asc())
|
|
.load::<User>(&mut db_conn)
|
|
.map_err(|e| format!("Query error: {e}"))
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(users)) => {
|
|
let mut html = String::from(r##"<div class="users-section">
|
|
<div class="section-header">
|
|
<h3>Users</h3>
|
|
<p class="text-muted">Select a user to manage their roles and groups</p>
|
|
</div>
|
|
<div class="search-box">
|
|
<input type="text" placeholder="Search users..."
|
|
hx-get="/settings/rbac/users"
|
|
hx-trigger="keyup changed delay:300ms"
|
|
hx-target=".user-list"
|
|
hx-include="this"
|
|
name="search" />
|
|
</div>
|
|
<div class="user-list">"##);
|
|
|
|
for user in &users {
|
|
let admin_badge = if user.is_admin {
|
|
r##"<span class="badge badge-system">Admin</span>"##
|
|
} else {
|
|
""
|
|
};
|
|
html.push_str(&format!(
|
|
r##"<div class="user-item" hx-get="/settings/rbac/users/{user_id}/assignment" hx-target="#assignment-panel" hx-swap="innerHTML">
|
|
<div class="user-info">
|
|
<span class="user-name">{username}</span>
|
|
<span class="user-email text-muted">{email}</span>
|
|
</div>
|
|
<div class="user-badges">{badges}</div>
|
|
</div>"##,
|
|
user_id = user.id,
|
|
username = user.username,
|
|
email = user.email,
|
|
badges = admin_badge
|
|
));
|
|
}
|
|
|
|
if users.is_empty() {
|
|
html.push_str(r##"<div class="empty-state">No users found</div>"##);
|
|
}
|
|
|
|
html.push_str(r##"</div><div id="assignment-panel"></div></div>"##);
|
|
Html(html).into_response()
|
|
}
|
|
Ok(Err(e)) => {
|
|
tracing::warn!(error = %e, "rbac_users_list query failed");
|
|
Html(render_error("rbac_users_list")).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "rbac_users_list task failed");
|
|
Html(render_error("rbac_users_list")).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn rbac_roles_list(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
|
let conn = state.conn.clone();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
let mut db_conn = conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
use crate::core::shared::models::schema::rbac_roles;
|
|
rbac_roles::table
|
|
.filter(rbac_roles::is_active.eq(true))
|
|
.order(rbac_roles::display_name.asc())
|
|
.load::<RbacRole>(&mut db_conn)
|
|
.map_err(|e| format!("Query error: {e}"))
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(roles)) => {
|
|
let mut html = String::from(r##"<div class="roles-section">
|
|
<div class="section-header">
|
|
<h3>Roles</h3>
|
|
<p class="text-muted">Manage roles and their permissions</p>
|
|
</div>
|
|
<form class="create-form" hx-post="/api/rbac/roles" hx-target=".role-list" hx-swap="beforeend" hx-on::after-request="this.reset()">
|
|
<input type="text" name="name" placeholder="Role name (e.g., editor)" required />
|
|
<input type="text" name="display_name" placeholder="Display name (e.g., Content Editor)" required />
|
|
<input type="text" name="description" placeholder="Description (optional)" />
|
|
<button type="submit" class="btn-primary">+ Add Role</button>
|
|
</form>
|
|
<div class="role-list">"##);
|
|
|
|
for role in &roles {
|
|
let badge_class = if role.is_system { "badge-system" } else { "badge-custom" };
|
|
let badge_text = if role.is_system { "System" } else { "Custom" };
|
|
let delete_btn = if role.is_system {
|
|
String::new()
|
|
} else {
|
|
format!(
|
|
r##"<button class="btn-danger" hx-delete="/api/rbac/roles/{role_id}" hx-target="closest .role-item" hx-swap="outerHTML" hx-confirm="Delete this role?">Delete</button>"##,
|
|
role_id = role.id
|
|
)
|
|
};
|
|
let description = role.description.as_deref().unwrap_or("");
|
|
html.push_str(&format!(
|
|
r##"<div class="role-item">
|
|
<div class="role-info">
|
|
<span class="role-name">{display_name}</span>
|
|
<span class="role-desc text-muted">{description}</span>
|
|
</div>
|
|
<div class="role-actions">
|
|
<span class="badge {badge_class}">{badge_text}</span>
|
|
{delete_btn}
|
|
</div>
|
|
</div>"##,
|
|
display_name = role.display_name,
|
|
description = description,
|
|
badge_class = badge_class,
|
|
badge_text = badge_text,
|
|
delete_btn = delete_btn
|
|
));
|
|
}
|
|
|
|
if roles.is_empty() {
|
|
html.push_str(r##"<div class="empty-state">No roles defined. Create your first role above.</div>"##);
|
|
}
|
|
|
|
html.push_str(r##"</div></div>"##);
|
|
Html(html).into_response()
|
|
}
|
|
Ok(Err(e)) => {
|
|
tracing::warn!(error = %e, "rbac_roles_list query failed");
|
|
Html(render_error("rbac_roles_list")).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "rbac_roles_list task failed");
|
|
Html(render_error("rbac_roles_list")).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn rbac_groups_list(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
|
let conn = state.conn.clone();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
let mut db_conn = conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
use crate::core::shared::models::schema::rbac_groups;
|
|
rbac_groups::table
|
|
.filter(rbac_groups::is_active.eq(true))
|
|
.order(rbac_groups::display_name.asc())
|
|
.load::<RbacGroup>(&mut db_conn)
|
|
.map_err(|e| format!("Query error: {e}"))
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(groups)) => {
|
|
let mut html = String::from(r##"<div class="groups-section">
|
|
<div class="section-header">
|
|
<h3>Groups</h3>
|
|
<p class="text-muted">Organize users into groups for easier role assignment</p>
|
|
</div>
|
|
<form class="create-form" hx-post="/api/rbac/groups" hx-target=".group-list" hx-swap="beforeend" hx-on::after-request="this.reset()">
|
|
<input type="text" name="name" placeholder="Group name (e.g., sales_team)" required />
|
|
<input type="text" name="display_name" placeholder="Display name (e.g., Sales Team)" required />
|
|
<input type="text" name="description" placeholder="Description (optional)" />
|
|
<button type="submit" class="btn-primary">+ Add Group</button>
|
|
</form>
|
|
<div class="group-list">"##);
|
|
|
|
for group in &groups {
|
|
let description = group.description.as_deref().unwrap_or("");
|
|
html.push_str(&format!(
|
|
r##"<div class="group-item">
|
|
<div class="group-info">
|
|
<span class="group-name">{display_name}</span>
|
|
<span class="group-desc text-muted">{description}</span>
|
|
</div>
|
|
<div class="group-actions">
|
|
<button class="btn-secondary" hx-get="/settings/rbac/groups/{group_id}/roles" hx-target="#group-roles-panel" hx-swap="innerHTML">Manage Roles</button>
|
|
<button class="btn-danger" hx-delete="/api/rbac/groups/{group_id}" hx-target="closest .group-item" hx-swap="outerHTML" hx-confirm="Delete this group?">Delete</button>
|
|
</div>
|
|
</div>"##,
|
|
display_name = group.display_name,
|
|
description = description,
|
|
group_id = group.id
|
|
));
|
|
}
|
|
|
|
if groups.is_empty() {
|
|
html.push_str(r##"<div class="empty-state">No groups defined. Create your first group above.</div>"##);
|
|
}
|
|
|
|
html.push_str(r##"</div><div id="group-roles-panel"></div></div>"##);
|
|
Html(html).into_response()
|
|
}
|
|
Ok(Err(e)) => {
|
|
tracing::warn!(error = %e, "rbac_groups_list query failed");
|
|
Html(render_error("rbac_groups_list")).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "rbac_groups_list task failed");
|
|
Html(render_error("rbac_groups_list")).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn user_assignment_panel(State(state): State<Arc<AppState>>, Path(user_id): Path<Uuid>) -> impl IntoResponse {
|
|
let conn = state.conn.clone();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
let mut db_conn = conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
use crate::core::shared::models::schema::users;
|
|
users::table
|
|
.find(user_id)
|
|
.first::<User>(&mut db_conn)
|
|
.map_err(|e| format!("User not found: {e}"))
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(user)) => {
|
|
let html = format!(
|
|
r##"<div class="assignment-container">
|
|
<h4>Manage Access for: {username}</h4>
|
|
<p class="text-muted">{email}</p>
|
|
|
|
<div class="assignment-tabs">
|
|
<button class="tab-btn active" onclick="showTab('roles-tab', this)">Roles</button>
|
|
<button class="tab-btn" onclick="showTab('groups-tab', this)">Groups</button>
|
|
</div>
|
|
|
|
<div id="roles-tab" class="tab-content active">
|
|
<div class="assignment-panel">
|
|
<div class="list-box">
|
|
<div class="list-box-header">Available Roles</div>
|
|
<div class="list-box-content" id="available-roles"
|
|
hx-get="/settings/rbac/users/{uid}/available-roles"
|
|
hx-trigger="load, roleAssigned from:body, roleRemoved from:body">
|
|
<div class="loading">Loading...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="transfer-buttons">
|
|
<button class="transfer-btn" id="assign-role-btn"
|
|
hx-post="/api/rbac/users/{uid}/roles/__ROLE_ID__"
|
|
hx-trigger="click"
|
|
hx-swap="none"
|
|
hx-on::after-request="htmx.trigger(document.body, 'roleAssigned')"
|
|
disabled>→</button>
|
|
<button class="transfer-btn" id="remove-role-btn"
|
|
hx-delete="/api/rbac/users/{uid}/roles/__ROLE_ID__"
|
|
hx-trigger="click"
|
|
hx-swap="none"
|
|
hx-on::after-request="htmx.trigger(document.body, 'roleRemoved')"
|
|
disabled>←</button>
|
|
</div>
|
|
|
|
<div class="list-box">
|
|
<div class="list-box-header">Assigned Roles</div>
|
|
<div class="list-box-content" id="assigned-roles"
|
|
hx-get="/settings/rbac/users/{uid}/assigned-roles"
|
|
hx-trigger="load, roleAssigned from:body, roleRemoved from:body">
|
|
<div class="loading">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="groups-tab" class="tab-content" style="display:none;">
|
|
<div class="assignment-panel">
|
|
<div class="list-box">
|
|
<div class="list-box-header">Available Groups</div>
|
|
<div class="list-box-content" id="available-groups"
|
|
hx-get="/settings/rbac/users/{uid}/available-groups"
|
|
hx-trigger="load, groupAdded from:body, groupRemoved from:body">
|
|
<div class="loading">Loading...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="transfer-buttons">
|
|
<button class="transfer-btn" id="add-group-btn"
|
|
hx-post="/api/rbac/users/{uid}/groups/__GROUP_ID__"
|
|
hx-trigger="click"
|
|
hx-swap="none"
|
|
hx-on::after-request="htmx.trigger(document.body, 'groupAdded')"
|
|
disabled>→</button>
|
|
<button class="transfer-btn" id="remove-group-btn"
|
|
hx-delete="/api/rbac/users/{uid}/groups/__GROUP_ID__"
|
|
hx-trigger="click"
|
|
hx-swap="none"
|
|
hx-on::after-request="htmx.trigger(document.body, 'groupRemoved')"
|
|
disabled>←</button>
|
|
</div>
|
|
|
|
<div class="list-box">
|
|
<div class="list-box-header">Member Of</div>
|
|
<div class="list-box-content" id="assigned-groups"
|
|
hx-get="/settings/rbac/users/{uid}/assigned-groups"
|
|
hx-trigger="load, groupAdded from:body, groupRemoved from:body">
|
|
<div class="loading">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function showTab(tabId, btn) {{
|
|
document.querySelectorAll('.tab-content').forEach(t => t.style.display = 'none');
|
|
document.querySelectorAll('.assignment-tabs .tab-btn').forEach(b => b.classList.remove('active'));
|
|
document.getElementById(tabId).style.display = 'block';
|
|
btn.classList.add('active');
|
|
}}
|
|
|
|
function selectAvailableRole(el, roleId) {{
|
|
document.querySelectorAll('#available-roles .list-item').forEach(i => i.classList.remove('selected'));
|
|
el.classList.add('selected');
|
|
const btn = document.getElementById('assign-role-btn');
|
|
btn.disabled = false;
|
|
btn.setAttribute('hx-post', '/api/rbac/users/{uid}/roles/' + roleId);
|
|
htmx.process(btn);
|
|
}}
|
|
|
|
function selectAssignedRole(el, roleId) {{
|
|
document.querySelectorAll('#assigned-roles .list-item').forEach(i => i.classList.remove('selected'));
|
|
el.classList.add('selected');
|
|
const btn = document.getElementById('remove-role-btn');
|
|
btn.disabled = false;
|
|
btn.setAttribute('hx-delete', '/api/rbac/users/{uid}/roles/' + roleId);
|
|
htmx.process(btn);
|
|
}}
|
|
|
|
function selectAvailableGroup(el, groupId) {{
|
|
document.querySelectorAll('#available-groups .list-item').forEach(i => i.classList.remove('selected'));
|
|
el.classList.add('selected');
|
|
const btn = document.getElementById('add-group-btn');
|
|
btn.disabled = false;
|
|
btn.setAttribute('hx-post', '/api/rbac/users/{uid}/groups/' + groupId);
|
|
htmx.process(btn);
|
|
}}
|
|
|
|
function selectAssignedGroup(el, groupId) {{
|
|
document.querySelectorAll('#assigned-groups .list-item').forEach(i => i.classList.remove('selected'));
|
|
el.classList.add('selected');
|
|
const btn = document.getElementById('remove-group-btn');
|
|
btn.disabled = false;
|
|
btn.setAttribute('hx-delete', '/api/rbac/users/{uid}/groups/' + groupId);
|
|
htmx.process(btn);
|
|
}}
|
|
</script>"##,
|
|
username = user.username,
|
|
email = user.email,
|
|
uid = user_id
|
|
);
|
|
Html(html).into_response()
|
|
}
|
|
Ok(Err(e)) => {
|
|
tracing::warn!(error = %e, "user_assignment_panel query failed");
|
|
Html(render_error("user_assignment_panel")).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "user_assignment_panel task failed");
|
|
Html(render_error("user_assignment_panel")).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn available_roles_for_user(State(state): State<Arc<AppState>>, Path(user_id): Path<Uuid>) -> impl IntoResponse {
|
|
let conn = state.conn.clone();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
let mut db_conn = conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
use crate::core::shared::models::schema::{rbac_roles, rbac_user_roles};
|
|
|
|
let assigned_role_ids: Vec<Uuid> = rbac_user_roles::table
|
|
.filter(rbac_user_roles::user_id.eq(user_id))
|
|
.select(rbac_user_roles::role_id)
|
|
.load(&mut db_conn)
|
|
.map_err(|e| format!("Query error: {e}"))?;
|
|
|
|
rbac_roles::table
|
|
.filter(rbac_roles::is_active.eq(true))
|
|
.filter(rbac_roles::id.ne_all(assigned_role_ids))
|
|
.order(rbac_roles::display_name.asc())
|
|
.load::<RbacRole>(&mut db_conn)
|
|
.map_err(|e| format!("Query error: {e}"))
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(roles)) => {
|
|
let mut html = String::new();
|
|
for role in &roles {
|
|
html.push_str(&format!(
|
|
r##"<div class="list-item" onclick="selectAvailableRole(this, '{id}')">{name}</div>"##,
|
|
id = role.id,
|
|
name = role.display_name
|
|
));
|
|
}
|
|
if roles.is_empty() {
|
|
html.push_str(r##"<div class="empty-state">All roles assigned</div>"##);
|
|
}
|
|
Html(html).into_response()
|
|
}
|
|
Ok(Err(e)) => {
|
|
tracing::warn!(error = %e, "available_roles_for_user query failed");
|
|
Html(render_error("available_roles_for_user")).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "available_roles_for_user task failed");
|
|
Html(render_error("available_roles_for_user")).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn assigned_roles_for_user(State(state): State<Arc<AppState>>, Path(user_id): Path<Uuid>) -> impl IntoResponse {
|
|
let conn = state.conn.clone();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
let mut db_conn = conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
use crate::core::shared::models::schema::{rbac_roles, rbac_user_roles};
|
|
|
|
rbac_user_roles::table
|
|
.inner_join(rbac_roles::table)
|
|
.filter(rbac_user_roles::user_id.eq(user_id))
|
|
.filter(rbac_roles::is_active.eq(true))
|
|
.select(RbacRole::as_select())
|
|
.load::<RbacRole>(&mut db_conn)
|
|
.map_err(|e| format!("Query error: {e}"))
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(roles)) => {
|
|
let mut html = String::new();
|
|
for role in &roles {
|
|
html.push_str(&format!(
|
|
r##"<div class="list-item" onclick="selectAssignedRole(this, '{id}')">{name}</div>"##,
|
|
id = role.id,
|
|
name = role.display_name
|
|
));
|
|
}
|
|
if roles.is_empty() {
|
|
html.push_str(r##"<div class="empty-state">No roles assigned</div>"##);
|
|
}
|
|
Html(html).into_response()
|
|
}
|
|
Ok(Err(e)) => {
|
|
tracing::warn!(error = %e, "assigned_roles_for_user query failed");
|
|
Html(render_error("assigned_roles_for_user")).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "assigned_roles_for_user task failed");
|
|
Html(render_error("assigned_roles_for_user")).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn available_groups_for_user(State(state): State<Arc<AppState>>, Path(user_id): Path<Uuid>) -> impl IntoResponse {
|
|
let conn = state.conn.clone();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
let mut db_conn = conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
use crate::core::shared::models::schema::{rbac_groups, rbac_user_groups};
|
|
|
|
let assigned_group_ids: Vec<Uuid> = rbac_user_groups::table
|
|
.filter(rbac_user_groups::user_id.eq(user_id))
|
|
.select(rbac_user_groups::group_id)
|
|
.load(&mut db_conn)
|
|
.map_err(|e| format!("Query error: {e}"))?;
|
|
|
|
rbac_groups::table
|
|
.filter(rbac_groups::is_active.eq(true))
|
|
.filter(rbac_groups::id.ne_all(assigned_group_ids))
|
|
.order(rbac_groups::display_name.asc())
|
|
.load::<RbacGroup>(&mut db_conn)
|
|
.map_err(|e| format!("Query error: {e}"))
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(groups)) => {
|
|
let mut html = String::new();
|
|
for group in &groups {
|
|
html.push_str(&format!(
|
|
r##"<div class="list-item" onclick="selectAvailableGroup(this, '{id}')">{name}</div>"##,
|
|
id = group.id,
|
|
name = group.display_name
|
|
));
|
|
}
|
|
if groups.is_empty() {
|
|
html.push_str(r##"<div class="empty-state">All groups joined</div>"##);
|
|
}
|
|
Html(html).into_response()
|
|
}
|
|
Ok(Err(e)) => {
|
|
tracing::warn!(error = %e, "available_groups_for_user query failed");
|
|
Html(render_error("available_groups_for_user")).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "available_groups_for_user task failed");
|
|
Html(render_error("available_groups_for_user")).into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn assigned_groups_for_user(State(state): State<Arc<AppState>>, Path(user_id): Path<Uuid>) -> impl IntoResponse {
|
|
let conn = state.conn.clone();
|
|
let result = tokio::task::spawn_blocking(move || {
|
|
let mut db_conn = conn.get().map_err(|e| format!("DB error: {e}"))?;
|
|
use crate::core::shared::models::schema::{rbac_groups, rbac_user_groups};
|
|
|
|
rbac_user_groups::table
|
|
.inner_join(rbac_groups::table)
|
|
.filter(rbac_user_groups::user_id.eq(user_id))
|
|
.filter(rbac_groups::is_active.eq(true))
|
|
.select(RbacGroup::as_select())
|
|
.load::<RbacGroup>(&mut db_conn)
|
|
.map_err(|e| format!("Query error: {e}"))
|
|
})
|
|
.await;
|
|
|
|
match result {
|
|
Ok(Ok(groups)) => {
|
|
let mut html = String::new();
|
|
for group in &groups {
|
|
html.push_str(&format!(
|
|
r##"<div class="list-item" onclick="selectAssignedGroup(this, '{id}')">{name}</div>"##,
|
|
id = group.id,
|
|
name = group.display_name
|
|
));
|
|
}
|
|
if groups.is_empty() {
|
|
html.push_str(r##"<div class="empty-state">Not a member of any group</div>"##);
|
|
}
|
|
Html(html).into_response()
|
|
}
|
|
Ok(Err(e)) => {
|
|
tracing::warn!(error = %e, "assigned_groups_for_user query failed");
|
|
Html(render_error("assigned_groups_for_user")).into_response()
|
|
}
|
|
Err(e) => {
|
|
tracing::warn!(error = %e, "assigned_groups_for_user task failed");
|
|
Html(render_error("assigned_groups_for_user")).into_response()
|
|
}
|
|
}
|
|
}
|