use axum::{
extract::{Path, Query},
http::Uri,
response::{IntoResponse, Redirect, Response},
};
use chrono::Utc;
use sea_orm::{
ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter,
QueryOrder,
};
use serde::Deserialize;
use crate::template::RenderAppPane;
use crate::{
components::{ObjectList, SharedChromeFolder, SlotCtx, SwapKey},
html_form::HtmlFormBody,
http::Cap,
plugins::users::{
entities::role::{self, Entity as RoleEntity},
keys::{
RoleCreateModalKey, RoleDeleteModalKey, RoleEditModalKey, RoleSelectTableKey,
RoleTableKey,
},
middleware::RequireStaff,
null_text::NullText,
routes::UsersRolesDetailRouteTag,
state::UsersState,
templates::{
ConfirmDeletePage, RoleCreateModalPage, RoleDetailPage, RoleEditModalPage,
RoleListPage, RoleOption, RoleSelectPage,
},
},
web::{
Htmx, QueryPageSize, html_built_page_or_app_layout, html_built_page_with_slots,
respond_create_modal_done_fk, respond_edit_modal_done,
},
};
use super::users::ModalNameQuery;
#[derive(Debug, Deserialize, Default)]
pub struct RoleListQuery {
#[serde(default, rename = "Name", alias = "name")]
pub name: Option<String>,
#[serde(default)]
pub sort: Option<String>,
#[serde(default)]
pub page: Option<u32>,
#[serde(default)]
pub page_size: QueryPageSize,
#[serde(default)]
pub target_input: Option<String>,
}
fn path_and_query(uri: &Uri) -> String {
uri.path_and_query()
.map(|pq| pq.as_str().to_string())
.unwrap_or_else(|| uri.path().to_string())
}
async fn load_roles_page(
db: &sea_orm::DatabaseConnection,
q: &RoleListQuery,
) -> ObjectList<RoleOption> {
let mut query = RoleEntity::find();
let name = q.name.clone().unwrap_or_default();
if !name.is_empty() {
query = query.filter(role::Column::Name.contains(&name));
}
let sort = q.sort.as_deref().unwrap_or("").trim();
let query = match sort {
s if s.eq_ignore_ascii_case("Name DESC") => query.order_by_desc(role::Column::Name),
s if s.eq_ignore_ascii_case("Name ASC") || s.eq_ignore_ascii_case("Name") => {
query.order_by_asc(role::Column::Name)
}
_ => query.order_by_asc(role::Column::Id),
};
let page = q.page.unwrap_or(1).max(1);
let page_size = q.page_size.get();
let paginator = query.paginate(db, page_size as u64);
let total = paginator.num_items().await.unwrap_or(0);
let models = paginator
.fetch_page((page as u64).saturating_sub(1))
.await
.unwrap_or_default();
let rows = models
.into_iter()
.map(|r| RoleOption {
id: r.id,
name: r.name.to_string(),
})
.collect();
ObjectList::from_page(rows, page, page_size, total)
}
pub async fn list(
Cap(state): Cap<UsersState>,
Cap(chrome): Cap<SharedChromeFolder>,
RequireStaff(ctx): RequireStaff,
htmx: Htmx,
uri: Uri,
Query(q): Query<RoleListQuery>,
) -> maud::Markup {
let roles = load_roles_page(&state.db, &q).await;
let page = RoleListPage {
roles,
filter_name: q.name.clone().unwrap_or_default(),
sort: q.sort.clone().unwrap_or_default(),
path_and_query: path_and_query(&uri),
page_size: q.page_size.get(),
};
if htmx.targets::<RoleTableKey>() {
return page.render_table();
}
if htmx.wants_main_content() {
return page.render_main().into();
}
if htmx.wants_app_layout() {
return page.render_pane().into();
}
html_built_page_with_slots(&page, &chrome, &SlotCtx::from_auth(&ctx))
}
pub async fn select(
Cap(state): Cap<UsersState>,
Cap(chrome): Cap<SharedChromeFolder>,
RequireStaff(ctx): RequireStaff,
htmx: Htmx,
uri: Uri,
Query(q): Query<RoleListQuery>,
) -> maud::Markup {
let roles = load_roles_page(&state.db, &q).await;
let page = RoleSelectPage {
roles,
filter_name: q.name.clone().unwrap_or_default(),
target_input: q.target_input.clone().unwrap_or_else(|| "RoleID".into()),
sort: q.sort.clone().unwrap_or_default(),
path_and_query: path_and_query(&uri),
page_size: q.page_size.get(),
};
if htmx.targets::<RoleSelectTableKey>() {
return page.render_table();
}
html_built_page_with_slots(&page, &chrome, &SlotCtx::from_auth(&ctx))
}
pub async fn detail(
Cap(state): Cap<UsersState>,
Cap(chrome): Cap<SharedChromeFolder>,
RequireStaff(ctx): RequireStaff,
htmx: Htmx,
Path(id): Path<i64>,
) -> Response {
let Some(role) = crate::web::opt_or_log(
RoleEntity::find_by_id(id).one(&state.db).await,
"find role by id",
) else {
return Redirect::to("/users/roles/").into_response();
};
let page = RoleDetailPage {
id: role.id,
name: role.name.to_string(),
};
html_built_page_or_app_layout(&page, &htmx, &chrome, &SlotCtx::from_auth(&ctx)).into_response()
}
pub async fn create_get(
Cap(chrome): Cap<SharedChromeFolder>,
RequireStaff(ctx): RequireStaff,
Query(q): Query<ModalNameQuery>,
) -> maud::Markup {
let page = RoleCreateModalPage {
form_name: q.form_name(),
refresh_table: q.refresh_table(),
target_input: q.target_input(),
name: String::new(),
error: String::new(),
};
html_built_page_with_slots(&page, &chrome, &SlotCtx::from_auth(&ctx))
}
use crate::plugins::users::forms::RoleForm;
pub async fn create_post(
Cap(state): Cap<UsersState>,
Cap(chrome): Cap<SharedChromeFolder>,
RequireStaff(ctx): RequireStaff,
htmx: Htmx,
Query(q): Query<ModalNameQuery>,
HtmlFormBody(form): HtmlFormBody<RoleForm>,
) -> Response {
let now = Utc::now();
let model = role::ActiveModel {
id: Default::default(),
created_at: Set(Some(now)),
updated_at: Set(Some(now)),
name: Set(NullText::from(form.name.clone())),
};
match model.insert(&state.db).await {
Ok(role) => respond_create_modal_done_fk::<RoleCreateModalKey>(
&htmx,
&q.refresh_table(),
&UsersRolesDetailRouteTag::new(role.id).url(),
role.id,
role.name.as_str(),
&q.target_input(),
),
Err(e) => {
let page = RoleCreateModalPage {
form_name: q.form_name(),
refresh_table: q.refresh_table(),
target_input: q.target_input(),
name: form.name,
error: e.to_string(),
};
html_built_page_with_slots(&page, &chrome, &SlotCtx::from_auth(&ctx)).into_response()
}
}
}
pub async fn edit_get(
Cap(state): Cap<UsersState>,
Cap(chrome): Cap<SharedChromeFolder>,
RequireStaff(ctx): RequireStaff,
Path(id): Path<i64>,
Query(q): Query<ModalNameQuery>,
) -> Response {
let Some(role) = crate::web::opt_or_log(
RoleEntity::find_by_id(id).one(&state.db).await,
"find role by id",
) else {
return Redirect::to("/users/roles/").into_response();
};
let page = RoleEditModalPage {
id: role.id,
form_name: q.form_name(),
name: role.name.to_string(),
error: String::new(),
};
html_built_page_with_slots(&page, &chrome, &SlotCtx::from_auth(&ctx)).into_response()
}
pub async fn edit_post(
Cap(state): Cap<UsersState>,
Cap(chrome): Cap<SharedChromeFolder>,
RequireStaff(ctx): RequireStaff,
htmx: Htmx,
Path(id): Path<i64>,
Query(q): Query<ModalNameQuery>,
HtmlFormBody(form): HtmlFormBody<RoleForm>,
) -> Response {
let Some(role) = crate::web::opt_or_log(
RoleEntity::find_by_id(id).one(&state.db).await,
"find role by id",
) else {
return Redirect::to("/users/roles/").into_response();
};
let mut am: role::ActiveModel = role.into();
am.name = Set(NullText::from(form.name.clone()));
am.updated_at = Set(Some(Utc::now()));
match am.update(&state.db).await {
Ok(_) => respond_edit_modal_done::<RoleEditModalKey>(
&htmx,
&UsersRolesDetailRouteTag::new(id).url(),
),
Err(e) => {
let page = RoleEditModalPage {
id,
form_name: q.form_name(),
name: form.name,
error: e.to_string(),
};
html_built_page_with_slots(&page, &chrome, &SlotCtx::from_auth(&ctx)).into_response()
}
}
}
pub async fn delete_get(
Cap(chrome): Cap<SharedChromeFolder>,
RequireStaff(ctx): RequireStaff,
Query(q): Query<ModalNameQuery>,
Path(id): Path<i64>,
) -> maud::Markup {
let page = ConfirmDeletePage {
modal_uid: RoleDeleteModalKey::ID.to_string(),
message: "Are you sure you want to delete this role?".into(),
form_name: q
.name
.clone()
.unwrap_or_else(|| "p_users.RoleDeleteForm".into()),
id,
error: String::new(),
};
html_built_page_with_slots(&page, &chrome, &SlotCtx::from_auth(&ctx))
}
pub async fn delete_post(
Cap(state): Cap<UsersState>,
Cap(chrome): Cap<SharedChromeFolder>,
RequireStaff(ctx): RequireStaff,
htmx: Htmx,
Path(id): Path<i64>,
) -> Response {
match RoleEntity::delete_by_id(id).exec(&state.db).await {
Ok(_) => htmx.redirect("/users/roles/"),
Err(e) => {
tracing::error!(error = %e, id, "failed to delete role");
let page = ConfirmDeletePage {
modal_uid: RoleDeleteModalKey::ID.to_string(),
message: "Are you sure you want to delete this role?".into(),
form_name: "p_users.RoleDeleteForm".into(),
id,
error: e.to_string(),
};
html_built_page_with_slots(&page, &chrome, &SlotCtx::from_auth(&ctx)).into_response()
}
}
}