use crate::app::AdminAppState;
use crate::auth::AdminUser;
use axum::{
body::Bytes,
extract::{Extension, Path},
http::{header::LOCATION, StatusCode},
response::{Html, IntoResponse, Response},
};
use serde::Serialize;
use std::sync::Arc;
use tower_cookies::Cookies;
use super::csrf::{get_or_create_csrf, validate_csrf};
use super::helpers::{build_nav, render_forbidden};
#[derive(Serialize)]
struct RoleRow {
name: String,
entity_count: usize,
}
#[derive(Serialize)]
struct RoleListContext {
admin_title: String,
admin_icon: String,
nav: Vec<crate::render::context::NavItem>,
current_entity: String,
show_auth_nav: bool,
roles: Vec<RoleRow>,
flash_error: Option<String>,
flash_success: Option<String>,
}
#[derive(Serialize)]
struct PermRow {
entity_name: String,
entity_label: String,
view: bool,
create: bool,
edit: bool,
delete: bool,
}
#[derive(Serialize)]
struct RoleFormContext {
admin_title: String,
admin_icon: String,
nav: Vec<crate::render::context::NavItem>,
current_entity: String,
show_auth_nav: bool,
role_name: Option<String>,
perms: Vec<PermRow>,
csrf_token: String,
error: Option<String>,
}
pub(super) async fn role_list(
Extension(state): Extension<Arc<AdminAppState>>,
Extension(user): Extension<AdminUser>,
) -> Response {
#[cfg(feature = "seaorm")]
if state.seaorm_auth.is_some() {
if !user.is_superuser {
return render_forbidden(&state, &user, "__roles").await;
}
return role_list_with_flash(&state, &user, None, None).await;
}
(StatusCode::NOT_FOUND, "Role management requires seaorm feature").into_response()
}
pub(super) async fn role_list_with_flash(
state: &Arc<AdminAppState>,
user: &crate::auth::AdminUser,
flash_error: Option<String>,
flash_success: Option<String>,
) -> Response {
#[cfg(feature = "seaorm")]
if let Some(ref seaorm) = state.seaorm_auth {
let role_names = seaorm.list_roles();
let rows: Vec<RoleRow> = role_names
.into_iter()
.map(|name| {
let perms = seaorm.get_role_permissions(&name);
let entity_count = perms
.iter()
.map(|(e, _)| e.clone())
.collect::<std::collections::HashSet<_>>()
.len();
RoleRow { name, entity_count }
})
.collect();
let ctx = RoleListContext {
admin_title: state.title.clone(),
admin_icon: state.icon.clone(),
nav: build_nav(state, "", user, state.enforcer.as_ref()).await,
current_entity: "__roles".to_string(),
show_auth_nav: state.show_auth_nav,
roles: rows,
flash_error,
flash_success,
};
return Html(state.renderer.render("roles.html", ctx)).into_response();
}
(StatusCode::NOT_FOUND, "Role management requires seaorm feature").into_response()
}
fn build_perm_rows(state: &AdminAppState, checked: &[(String, String)]) -> Vec<PermRow> {
state
.entities
.iter()
.map(|e| {
let has = |action: &str| {
checked
.iter()
.any(|(ent, act)| ent == &e.entity_name && act == action)
};
PermRow {
entity_name: e.entity_name.clone(),
entity_label: e.label.clone(),
view: has("view"),
create: has("create"),
edit: has("edit"),
delete: has("delete"),
}
})
.collect()
}
pub(super) async fn role_create_form(
cookies: Cookies,
Extension(state): Extension<Arc<AdminAppState>>,
Extension(user): Extension<AdminUser>,
) -> Response {
#[cfg(feature = "seaorm")]
if let Some(ref _seaorm) = state.seaorm_auth {
if !user.is_superuser {
return render_forbidden(&state, &user, "__roles").await;
}
let perms = build_perm_rows(&state, &[]);
let ctx = RoleFormContext {
admin_title: state.title.clone(),
admin_icon: state.icon.clone(),
nav: build_nav(&state, "", &user, state.enforcer.as_ref()).await,
current_entity: "__roles".to_string(),
show_auth_nav: state.show_auth_nav,
role_name: None,
perms,
csrf_token: get_or_create_csrf(&cookies),
error: None,
};
return Html(state.renderer.render("role_form.html", ctx)).into_response();
}
(StatusCode::NOT_FOUND, "Role management requires seaorm feature").into_response()
}
pub(super) async fn role_create_submit(
cookies: Cookies,
Extension(state): Extension<Arc<AdminAppState>>,
Extension(user): Extension<AdminUser>,
body: Bytes,
) -> Response {
#[cfg(feature = "seaorm")]
if let Some(ref seaorm) = state.seaorm_auth {
if !user.is_superuser {
return render_forbidden(&state, &user, "__roles").await;
}
let pairs: Vec<(String, String)> = form_urlencoded::parse(&body)
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect();
let csrf_form = pairs.iter().find(|(k, _)| k == "csrf_token").map(|(_, v)| v.as_str());
if !validate_csrf(&cookies, csrf_form) {
return (StatusCode::FORBIDDEN, "Invalid CSRF token").into_response();
}
let role_name_raw = pairs.iter()
.find(|(k, _)| k == "name")
.map(|(_, v)| v.clone())
.unwrap_or_default();
let perm_strings: Vec<String> = pairs.iter()
.filter(|(k, _)| k == "perms")
.map(|(_, v)| v.clone())
.collect();
let trimmed = role_name_raw.trim();
let name = if trimmed.is_empty() {
let perms = build_perm_rows(&state, &[]);
let ctx = RoleFormContext {
admin_title: state.title.clone(),
admin_icon: state.icon.clone(),
nav: build_nav(&state, "", &user, state.enforcer.as_ref()).await,
current_entity: "__roles".to_string(),
show_auth_nav: state.show_auth_nav,
role_name: None,
perms,
csrf_token: get_or_create_csrf(&cookies),
error: Some("Role name is required".to_string()),
};
return Html(state.renderer.render("role_form.html", ctx)).into_response();
} else {
trimmed.to_string()
};
let permissions: Vec<(String, String)> = perm_strings
.iter()
.filter_map(|p| {
let mut parts = p.splitn(2, '.');
let entity = parts.next()?.to_string();
let action = parts.next()?.to_string();
Some((entity, action))
})
.collect();
match seaorm.create_role(&name, &permissions).await {
Ok(_) => {
return (StatusCode::FOUND, [(LOCATION, "/admin/roles/")]).into_response();
}
Err(e) => {
let perms = build_perm_rows(&state, &permissions);
let ctx = RoleFormContext {
admin_title: state.title.clone(),
admin_icon: state.icon.clone(),
nav: build_nav(&state, "", &user, state.enforcer.as_ref()).await,
current_entity: "__roles".to_string(),
show_auth_nav: state.show_auth_nav,
role_name: Some(name),
perms,
csrf_token: get_or_create_csrf(&cookies),
error: Some(e.to_string()),
};
return Html(state.renderer.render("role_form.html", ctx)).into_response();
}
}
}
(StatusCode::NOT_FOUND, "Role management requires seaorm feature").into_response()
}
pub(super) async fn role_edit_form(
cookies: Cookies,
Path(role): Path<String>,
Extension(state): Extension<Arc<AdminAppState>>,
Extension(user): Extension<AdminUser>,
) -> Response {
#[cfg(feature = "seaorm")]
if let Some(ref seaorm) = state.seaorm_auth {
if !user.is_superuser {
return render_forbidden(&state, &user, "__roles").await;
}
let current_perms = seaorm.get_role_permissions(&role);
let perms = build_perm_rows(&state, ¤t_perms);
let ctx = RoleFormContext {
admin_title: state.title.clone(),
admin_icon: state.icon.clone(),
nav: build_nav(&state, "", &user, state.enforcer.as_ref()).await,
current_entity: "__roles".to_string(),
show_auth_nav: state.show_auth_nav,
role_name: Some(role),
perms,
csrf_token: get_or_create_csrf(&cookies),
error: None,
};
return Html(state.renderer.render("role_edit_form.html", ctx)).into_response();
}
(StatusCode::NOT_FOUND, "Role management requires seaorm feature").into_response()
}
pub(super) async fn role_edit_submit(
cookies: Cookies,
Path(role): Path<String>,
Extension(state): Extension<Arc<AdminAppState>>,
Extension(user): Extension<AdminUser>,
body: Bytes,
) -> Response {
#[cfg(feature = "seaorm")]
if let Some(ref seaorm) = state.seaorm_auth {
if !user.is_superuser {
return render_forbidden(&state, &user, "__roles").await;
}
let pairs: Vec<(String, String)> = form_urlencoded::parse(&body)
.map(|(k, v)| (k.into_owned(), v.into_owned()))
.collect();
let csrf_form = pairs.iter().find(|(k, _)| k == "csrf_token").map(|(_, v)| v.as_str());
if !validate_csrf(&cookies, csrf_form) {
return (StatusCode::FORBIDDEN, "Invalid CSRF token").into_response();
}
let perm_strings: Vec<String> = pairs.iter()
.filter(|(k, _)| k == "perms")
.map(|(_, v)| v.clone())
.collect();
let permissions: Vec<(String, String)> = perm_strings
.iter()
.filter_map(|p| {
let mut parts = p.splitn(2, '.');
let entity = parts.next()?.to_string();
let action = parts.next()?.to_string();
Some((entity, action))
})
.collect();
match seaorm.update_role_permissions(&role, &permissions).await {
Ok(_) => {
return (StatusCode::FOUND, [(LOCATION, "/admin/roles/")]).into_response();
}
Err(e) => {
let perms = build_perm_rows(&state, &permissions);
let ctx = RoleFormContext {
admin_title: state.title.clone(),
admin_icon: state.icon.clone(),
nav: build_nav(&state, "", &user, state.enforcer.as_ref()).await,
current_entity: "__roles".to_string(),
show_auth_nav: state.show_auth_nav,
role_name: Some(role),
perms,
csrf_token: get_or_create_csrf(&cookies),
error: Some(e.to_string()),
};
return Html(state.renderer.render("role_edit_form.html", ctx)).into_response();
}
}
}
(StatusCode::NOT_FOUND, "Role management requires seaorm feature").into_response()
}
pub(super) async fn role_delete(
Path(role): Path<String>,
Extension(state): Extension<Arc<AdminAppState>>,
Extension(user): Extension<AdminUser>,
) -> Response {
#[cfg(feature = "seaorm")]
if let Some(ref seaorm) = state.seaorm_auth {
if !user.is_superuser {
return render_forbidden(&state, &user, "__roles").await;
}
match seaorm.delete_role(&role).await {
Ok(_) => {
return role_list_with_flash(&state, &user, None, Some(format!("Role '{}' deleted.", role))).await;
}
Err(e) => {
return role_list_with_flash(&state, &user, Some(e.to_string()), None).await;
}
}
}
(StatusCode::NOT_FOUND, "Role management requires seaorm feature").into_response()
}