//! Handlers de administración de usuarios. use serde::Deserialize; use pagetop::prelude::*; use pagetop_htmx::prelude::*; use crate::ADMIN_USERS_PATH; use crate::ANONYMOUS_ROLE_ID; use crate::AUTHENTICATED_ROLE_ID; use crate::LOCALES_USER; use crate::account::{Account, UserStatus}; use crate::component::admin::{ AdminPasswordForm, USER_ADMIN_FORM_ID, UserForm, UserFormMode, UserTable, status_key, }; use crate::component::{language_name, multiline_text, theme_name}; use crate::config::{SETTINGS, user_language_applies, user_timezone_applies}; use crate::entity::{role, user}; use crate::error::AuthError; use crate::handlers::admin::{back_link, frame, map_auth_error}; use crate::password; use crate::permission::UserPermission; use crate::role_path; use crate::service::role_admin; use crate::service::user_admin::{self, UserListParams, UserSortField}; use crate::user_path; #[derive(Deserialize)] pub(crate) struct UsersQuery { #[serde(default)] q: Option, #[serde(default)] sort: Option, #[serde(default)] dir: Option, #[serde(default)] page: Option, } // **< list_get >*********************************************************************************** /// GET /admin/user/users - Listado de usuarios (búsqueda, orden y paginación vía HTMX). pub(crate) async fn list_get( request: HttpRequest, web::Query(query): web::Query, ) -> Result { require_permission(&request, &UserPermission::AdminUsers)?; let params = UserListParams { query: query.q.clone(), sort: UserSortField::from_query(query.sort.as_deref()), dir: SortDir::from_query(query.dir.as_deref()), page: query.page.unwrap_or(1).max(1), per_page: SETTINGS.admin.list_page_size, }; let result = match user_admin::list_users(¶ms).await { Ok(result) => result, Err(_) => return Err(ErrorPage::InternalError(Some(request))), }; let mut table = UserTable::new() .with_items(result.items) .with_sort(params.sort) .with_dir(params.dir) .with_query(query.q.clone()) .with_page(result.page) .with_per_page(result.per_page) .with_total(result.total); if request.is_htmx() { let mut cx = Context::admin(request); Ok(HtmxResponse::new(table.render(&mut cx).await).into_response()) } else { let title = Lc::t("title-admin-users", &LOCALES_USER); Ok(Page::admin(request) .with_title(title.clone()) .with_child( frame(title) .with_child(search_bar(query.q)) .with_child(table), ) .render() .await .into_response()) } } // **< search_bar >********************************************************************************* fn search_bar(current_query: Option) -> Html { let value = current_query.unwrap_or_default(); Html::with(move |cx| { html! { div class="user-admin-search" { input type="search" id="user-admin-search-input" name="q" value=(value.as_str()) placeholder=[Lc::t("field-search-users", &LOCALES_USER).lookup(cx)] hx-get=(cx.route(ADMIN_USERS_PATH).to_string()) hx-trigger="keyup changed delay:400ms, search" hx-target="#user-table-wrapper" hx-swap=(hx::swap::OUTER_HTML_SCROLL_TOP) hx-push-url="true"; } } }) } // **< available_roles >**************************************************************************** // Roles asignables desde la UI de usuarios: excluye "anonymous" (nunca se asigna explícitamente) // y "authenticated" (implícito, nunca se asigna). async fn available_roles(selected: &[i32]) -> Result, AuthError> { let items = role_admin::list_roles(&role_admin::RoleListParams { sort: role_admin::RoleSortField::Weight, dir: SortDir::Asc, }) .await?; Ok(items .into_iter() .filter(|r| r.id != ANONYMOUS_ROLE_ID && r.id != AUTHENTICATED_ROLE_ID) .map(|r| { let checked = selected.contains(&r.id); (r.id, r.label, checked) }) .collect()) } // Identificadores de rol enviados en el formulario de alta o edición. fn parse_role_ids(role_ids: &[String]) -> Vec { role_ids.iter().filter_map(|s| s.parse().ok()).collect() } // **< require_manageable >************************************************************************* // Carga el usuario y comprueba que quien hace la petición puede gestionarlo. La cuenta de un // administrador sólo la gestiona otro administrador: si no, quien administra usuarios podría // apoderarse de ella (p. ej. restableciendo su contraseña) y obtener acceso irrestricto, que sólo // un administrador puede conceder. async fn require_manageable(request: &HttpRequest, id: i32) -> Result { let Ok(user) = user_admin::find_user(id).await else { return Err(ErrorPage::NotFound(Some(request.clone()))); }; if user.is_admin && !request.extension::().is_some_and(|a| a.is_admin()) { return Err(ErrorPage::AccessDenied(Some(request.clone()))); } Ok(user) } // **< new_get / new_post >************************************************************************* /// GET /admin/user/users/new - Formulario de alta de usuario. pub(crate) async fn new_get( request: HttpRequest, web::Query(waypoint): web::Query, ) -> Result { require_permission(&request, &UserPermission::AdminUsers)?; let roles = match available_roles(&[]).await { Ok(roles) => roles, Err(_) => return Err(ErrorPage::InternalError(Some(request))), }; // El campo "administrador" sólo se ofrece si quien da de alta ya es administrador: no es un // permiso del catálogo (igual que conceder/revocar en la edición, ver `set_user_admin`). let allow_admin_field = request.extension::().is_some_and(|a| a.is_admin()); let mut page = Page::admin(request); let back_href = waypoint.or(page.context().route(ADMIN_USERS_PATH)); let title = Lc::t("title-admin-user-new", &LOCALES_USER); Ok(page .with_title(title.clone()) .with_child( frame(title) .with_child( UserForm::new() .with_mode(UserFormMode::New) .with_roles(roles) .with_allow_admin_field(allow_admin_field) .with_waypoint(waypoint), ) .with_child(back_link(back_href)), ) .render() .await .into_response()) } #[derive(Deserialize)] pub(crate) struct NewUserFormData { username: String, email: String, password: String, confirm_password: String, #[serde(default)] display_name: String, #[serde(default)] language: String, #[serde(default)] timezone: String, #[serde(default)] theme: String, #[serde(default)] role_ids: Vec, #[serde(default)] is_admin: bool, } /// POST /admin/user/users/new - Crea un usuario. /// /// Usa `RawForm` + `serde_qs` en lugar de `axum::extract::Form` (basado en `serde_urlencoded`, /// que no deserializa claves repetidas como `role_ids=2&role_ids=3` en un `Vec`). pub(crate) async fn new_post( request: HttpRequest, web::Query(waypoint): web::Query, raw: web::RawForm, ) -> Result { require_permission(&request, &UserPermission::AdminUsers)?; let Ok(form) = serde_qs::from_bytes::(&raw.0) else { return Err(ErrorPage::BadRequest(Some(request))); }; let allow_admin_field = request.extension::().is_some_and(|a| a.is_admin()); // Nunca fiarse sólo de que el campo esté presente en el formulario: sólo se concede si quien // envía la petición ya es administrador, aunque alguien manipulase la petición a mano. let is_admin = form.is_admin && allow_admin_field; let role_ids = parse_role_ids(&form.role_ids); let result = user_admin::create_user(user_admin::NewUserData { username: &form.username, email: &form.email, password: &form.password, confirm_password: &form.confirm_password, display_name: util::non_blank(&form.display_name), language: util::non_blank(&form.language), timezone: util::non_blank(&form.timezone), theme: util::non_blank(&form.theme), initial_role_ids: &role_ids, is_admin, }) .await; match result { Ok(_) => { let cx = Context::admin(request); let target = waypoint.or(cx.route(ADMIN_USERS_PATH)); Ok(Redirect::see_other(target).into_response()) } Err(err) => { let roles = available_roles(&role_ids).await.unwrap_or_default(); let mut page = Page::admin(request); let back_href = waypoint.or(page.context().route(ADMIN_USERS_PATH)); let form_component = UserForm::new() .with_mode(UserFormMode::New) .with_username(form.username) .with_email(form.email) .with_display_name(form.display_name) .with_language(form.language) .with_timezone(form.timezone) .with_theme(form.theme) .with_roles(roles) .with_allow_admin_field(allow_admin_field) .with_is_admin(is_admin) .with_error(map_auth_error(&err)) .with_waypoint(waypoint); let title = Lc::t("title-admin-user-new", &LOCALES_USER); Ok(page .with_title(title.clone()) .with_child( frame(title) .with_child(form_component) .with_child(back_link(back_href)), ) .render() .await .into_response()) } } } // **< edit_get / edit_post >*********************************************************************** // Pantalla de edición con el formulario relleno con los datos guardados del usuario. async fn render_user_edit( request: HttpRequest, id: i32, error: Option, waypoint: Waypoint, ) -> Response { let user = match user_admin::find_user(id).await { Ok(user) => user, Err(_) => return ErrorPage::NotFound(Some(request)).into_response(), }; let current = match user_admin::user_role_ids(id).await { Ok(ids) => ids, Err(_) => return ErrorPage::InternalError(Some(request)).into_response(), }; let roles = match available_roles(¤t).await { Ok(roles) => roles, Err(_) => return ErrorPage::InternalError(Some(request)).into_response(), }; let status = UserStatus::from_i16(user.status); let is_admin = user.is_admin; let form = UserForm::new() .with_username(user.username) .with_email(user.email) .with_display_name(user.display_name.unwrap_or_default()) .with_about(user.about.unwrap_or_default()) .with_language(user.language.unwrap_or_default()) .with_timezone(user.timezone.unwrap_or_default()) .with_theme(user.theme.unwrap_or_default()) .with_roles(roles) .with_error(error); render_edit_page(request, id, status, is_admin, form, waypoint).await } // Pantalla de edición del usuario `id` con `form` ya relleno, sea con los datos guardados o con los // enviados que no se pudieron guardar. Siempre lleva la botonera de `edit_actions()`, porque en // modo `Edit` el botón "Guardar" está en ella y no dentro del formulario. async fn render_edit_page( request: HttpRequest, id: i32, status: UserStatus, is_admin: bool, form: UserForm, waypoint: Waypoint, ) -> Response { // El botón de conceder/revocar sólo se muestra si quien lo ve ya es administrador y no está // viendo su propio perfil: no es un permiso del catálogo, y nadie puede automodificarse el flag // (ver `set_user_admin`). let can_toggle_admin = request .extension::() .is_some_and(|a| a.is_admin() && a.id() != id); let mut page = Page::admin(request); let back_href = waypoint.or(page.context().route(ADMIN_USERS_PATH)); let title = Lc::t("title-admin-user-edit", &LOCALES_USER); let actions = edit_actions( id, status, is_admin, can_toggle_admin, &waypoint, page.context(), ); page.with_title(title.clone()) .with_child( frame(title) .with_child( form.with_mode(UserFormMode::Edit) .with_user_id(Some(id)) .with_waypoint(waypoint.clone()), ) .with_child(actions) .with_child(back_link(back_href)), ) .render() .await .into_response() } // Enlace a la pantalla de restablecer contraseña y botones de bloqueo/activación y de // concesión/revocación de acceso irrestricto (este último sólo si `can_toggle_admin`). El listado // de origen (`waypoint`) se arrastra a todas ellas para que, al volver aquí, esta misma pantalla // siga sabiendo devolver al listado en el estado en que se dejó. // // "Guardar" (envía el `
` de `UserForm` vía el atributo `form`, ver `USER_ADMIN_FORM_ID`) y // "Restablecer contraseña" son botones sueltos; bloqueo/activación y concesión/revocación de admin // (este último sólo si `can_toggle_admin`) van cada uno en su propio `Form`, con un campo `Hidden` // para el nuevo valor, para conservar el envío nativo sin JavaScript, mejorado con // `hx-post`/`hx-confirm`. Todos son hijos directos del mismo `Container` con Flex, que los alinea // en fila con espaciado uniforme sin que ninguno tenga que ser forzosamente un `Button` suelto (ver // PAGETOP.md, "Preferir componentes a `html!` en bruto"). fn edit_actions( user_id: i32, status: UserStatus, target_is_admin: bool, can_toggle_admin: bool, waypoint: &Waypoint, cx: &mut Context, ) -> Flex { let (next_status, label_key) = match status { UserStatus::Blocked => ("active", "btn-activate"), _ => ("blocked", "btn-block"), }; let (next_is_admin, admin_label_key, admin_confirm_key) = if target_is_admin { ("false", "btn-revoke-admin", "confirm-revoke-admin") } else { ("true", "btn-grant-admin", "confirm-grant-admin") }; let password_href = waypoint.append_to(cx.route(user_path(user_id, "password"))); let status_action = waypoint.append_to(cx.route(user_path(user_id, "status"))); let admin_action = waypoint.append_to(cx.route(user_path(user_id, "admin"))); let mut status_form = Form::new() .with_action(status_action.clone()) .with_method(form::Method::Post) .with_prop(PropsOp::set(hx::POST, status_action.to_string())) .with_child(form::Hidden::field("status", next_status)) .with_child( Button::submit(Lc::t(label_key, &LOCALES_USER)) .with_style(button::Style::Solid(Intent::Warning)), ); if let Some(confirm) = Lc::t("confirm-change-status", &LOCALES_USER).lookup(cx) { status_form = status_form.with_prop(PropsOp::set(hx::CONFIRM, confirm)); } let mut container = Flex::new() .with_wrap(flex::Behavior::Wrap) .with_align(align::Items::Center) .with_gap(align::Gap::Both(UnitValue::RelRem(0.5))) .with_child( Button::submit(Lc::t("btn-save", &LOCALES_USER)) .with_style(button::Style::Solid(Intent::Primary)) .with_prop(PropsOp::set("form", USER_ADMIN_FORM_ID)), ) .with_child( Button::anchor(Lc::t("btn-reset-password", &LOCALES_USER), password_href) .with_style(button::Style::Solid(Intent::Neutral)), ) .with_child(status_form); if can_toggle_admin { let admin_button = Button::submit(Lc::t(admin_label_key, &LOCALES_USER)) .with_style(button::Style::Solid(Intent::Severe)); let mut admin_form = Form::new() .with_action(admin_action.clone()) .with_method(form::Method::Post) .with_prop(PropsOp::set(hx::POST, admin_action.to_string())) .with_child(form::Hidden::field("is_admin", next_is_admin)) .with_child(admin_button); if let Some(confirm) = Lc::t(admin_confirm_key, &LOCALES_USER).lookup(cx) { admin_form = admin_form.with_prop(PropsOp::set(hx::CONFIRM, confirm)); } container = container.with_child(admin_form); } container } /// GET /admin/user/users/{id}/edit - Formulario de edición de usuario. pub(crate) async fn edit_get( request: HttpRequest, web::Path(id): web::Path, web::Query(waypoint): web::Query, ) -> Result { require_permission(&request, &UserPermission::AdminUsers)?; require_manageable(&request, id).await?; Ok(render_user_edit(request, id, None, waypoint).await) } #[derive(Deserialize)] pub(crate) struct EditUserFormData { username: String, email: String, #[serde(default)] display_name: String, #[serde(default)] about: String, #[serde(default)] language: String, #[serde(default)] timezone: String, #[serde(default)] theme: String, #[serde(default)] role_ids: Vec, } /// POST /admin/user/users/{id}/edit - Actualiza los datos de perfil y los roles de un usuario. /// /// Usa `RawForm` + `serde_qs` por los roles, igual que `new_post`. pub(crate) async fn edit_post( request: HttpRequest, web::Path(id): web::Path, web::Query(waypoint): web::Query, raw: web::RawForm, ) -> Result { require_permission(&request, &UserPermission::AdminUsers)?; let user = require_manageable(&request, id).await?; let Ok(form) = serde_qs::from_bytes::(&raw.0) else { return Err(ErrorPage::BadRequest(Some(request))); }; let role_ids = parse_role_ids(&form.role_ids); let mut result = user_admin::update_user( id, user_admin::UserUpdateData { username: &form.username, email: &form.email, display_name: util::non_blank(&form.display_name), about: util::non_blank(&form.about), language: util::non_blank(&form.language), timezone: util::non_blank(&form.timezone), theme: util::non_blank(&form.theme), }, ) .await; if result.is_ok() { result = user_admin::set_user_roles(id, &role_ids).await; } match result { Ok(()) => { let cx = Context::admin(request); let target = waypoint.or(cx.route(ADMIN_USERS_PATH)); Ok(Redirect::see_other(target).into_response()) } Err(err) => { let roles = available_roles(&role_ids).await.unwrap_or_default(); let status = UserStatus::from_i16(user.status); let form = UserForm::new() .with_username(form.username) .with_email(form.email) .with_display_name(form.display_name) .with_about(form.about) .with_language(form.language) .with_timezone(form.timezone) .with_theme(form.theme) .with_roles(roles) .with_error(map_auth_error(&err)); Ok(render_edit_page(request, id, status, user.is_admin, form, waypoint).await) } } } // **< view_get >*********************************************************************************** /// GET /admin/user/users/{id}/view - Pantalla de sólo lectura con todos los datos del usuario, /// incluidos los roles asignados. pub(crate) async fn view_get( request: HttpRequest, web::Path(id): web::Path, web::Query(waypoint): web::Query, ) -> Result { require_permission(&request, &UserPermission::AdminUsers)?; let user = match user_admin::find_user(id).await { Ok(user) => user, Err(_) => return Err(ErrorPage::NotFound(Some(request))), }; let roles = match user_admin::user_roles(id).await { Ok(roles) => roles, Err(_) => return Err(ErrorPage::InternalError(Some(request))), }; let status = UserStatus::from_i16(user.status); let mut page = Page::admin(request); let back_href = waypoint.or(page.context().route(ADMIN_USERS_PATH)); let details_block = user_view_details(&user, status, page.context()).await; let roles_block = user_view_roles(&roles, user.is_admin, page.context()).await; let title = Lc::t("title-admin-user-view", &LOCALES_USER); Ok(page .with_title(title.clone()) .with_child( frame(title) .with_child(details_block) .with_child(roles_block) .with_child(back_link(back_href)), ) .render() .await .into_response()) } // Bloque de sólo lectura con los datos de perfil del usuario. async fn user_view_details(user: &user::Model, status: UserStatus, cx: &mut Context) -> Block { let mut table = Table::new() .with_prop(PropsOp::add_classes("user-admin-table")) .with_row( table::Row::new() .with_cell(Lc::t("field-username-admin", &LOCALES_USER)) .with_cell(user.username.as_str()), ) .with_row( table::Row::new() .with_cell(Lc::t("field-email", &LOCALES_USER)) .with_cell(user.email.as_str()), ) .with_row( table::Row::new() .with_cell(Lc::t("field-display-name", &LOCALES_USER)) .with_cell(user.display_name.as_deref().unwrap_or("-")), ) .with_row( table::Row::new() .with_cell(Lc::t("field-about", &LOCALES_USER)) .with_cell(multiline_text( user.about.clone().unwrap_or_else(|| "-".into()), )), ); // Sólo se muestran si se aplican, igual que en el formulario de edición. if user_language_applies() { table = table.with_row( table::Row::new() .with_cell(Lc::t("field-language", &LOCALES_USER)) .with_cell(language_name(user.language.as_deref())), ); } if user_timezone_applies() { table = table.with_row( table::Row::new() .with_cell(Lc::t("field-timezone", &LOCALES_USER)) .with_cell(user.timezone.as_deref().unwrap_or("-")), ); } table = table .with_row( table::Row::new() .with_cell(Lc::t("field-theme", &LOCALES_USER)) .with_cell(theme_name(user.theme.as_deref())), ) .with_row( table::Row::new() .with_cell(Lc::t("col-status", &LOCALES_USER)) .with_cell(Lc::t(status_key(status), &LOCALES_USER)), ) .with_row( table::Row::new() .with_cell(Lc::t("field-member-since", &LOCALES_USER)) .with_cell(cx.format_date( user.created_at.with_timezone(&cx.timezone()).date_naive(), DateFormat::Long, )), ); if user.is_admin { let badge = Badge::severe(Lc::t("badge-admin", &LOCALES_USER)) .render(cx) .await; table = table.with_row( table::Row::new() .with_cell("") .with_cell(Html::with(move |_| badge.clone())), ); } Block::new() .with_title(Lc::t("title-user-details", &LOCALES_USER)) .with_child(table) } // Bloque de sólo lectura con los roles asignados al usuario, cada uno enlazado a su propia // pantalla de vista. async fn user_view_roles(roles: &[role::Model], is_admin: bool, cx: &mut Context) -> Block { // Un bloque sin hijos no se renderiza. if roles.is_empty() { return Block::new(); } let mut table = Table::new().with_prop(PropsOp::add_classes("user-admin-table")); for r in roles { let system_badge = if r.locked { Some( Badge::warning(Lc::t("badge-system-role", &LOCALES_USER)) .render(cx) .await, ) } else { None }; let (id, label) = (r.id, r.label.clone()); table = table.with_row( table::Row::new() .with_cell(Html::with(move |cx| { html! { a href=(cx.route(role_path(id, "view"))) { (label.as_str()) } } })) .with_cell(r.machine_name.as_str()) .with_cell(Html::with(move |_| { html! { @if let Some(badge) = &system_badge { (badge) } } })), ); } let mut block = Block::new() .with_title(Lc::t("field-roles", &LOCALES_USER)) .with_child(table); if is_admin { block = block.with_child(Html::with(|cx| { html! { p { (Lc::t("help-admin-roles", &LOCALES_USER).using(cx)) } } })); } block } // Cabecera con los datos básicos del usuario en la pantalla de restablecimiento de contraseña, que // de otro modo no lo identifica. fn user_summary(user: &user::Model) -> Table { Table::new() .with_prop(PropsOp::add_classes("user-admin-table")) .with_row( table::Row::new() .with_cell(Lc::t("field-username-admin", &LOCALES_USER)) .with_cell(user.username.as_str()), ) .with_row( table::Row::new() .with_cell(Lc::t("field-display-name", &LOCALES_USER)) .with_cell(user.display_name.as_deref().unwrap_or("-")), ) } // **< status_post >******************************************************************************** #[derive(Deserialize)] pub(crate) struct StatusFormData { status: String, } /// POST /admin/user/users/{id}/status - Bloquea o activa una cuenta. pub(crate) async fn status_post( request: HttpRequest, web::Path(id): web::Path, web::Query(waypoint): web::Query, web::Form(form): web::Form, ) -> Result { require_permission(&request, &UserPermission::AdminUsers)?; require_permission(&request, &UserPermission::BlockAccounts)?; require_manageable(&request, id).await?; let Some(account) = request.extension::().cloned() else { return Err(ErrorPage::AccessDenied(Some(request))); }; let new_status = match form.status.as_str() { "blocked" => UserStatus::Blocked, _ => UserStatus::Active, }; // Ver comentario equivalente en `admin_post`: el botón envía la petición vía `hx-post` (para el // diálogo `hx-confirm`) sin `hx-target`, así que un `Redirect` normal terminaría anidado dentro // del propio ``. `HtmxResponse::redirect()` fuerza una navegación real en el cliente. let is_htmx = request.is_htmx(); match user_admin::set_user_status(id, new_status, account.id()).await { Ok(()) => { let cx = Context::admin(request); let edit_href = waypoint.append_to(cx.route(user_path(id, "edit"))); if is_htmx { Ok(HtmxResponse::empty().redirect(edit_href).into_response()) } else { Ok(Redirect::see_other(edit_href).into_response()) } } Err(err) => Ok(render_user_edit(request, id, Some(map_auth_error(&err)), waypoint).await), } } // **< admin_post >********************************************************************************* #[derive(Deserialize)] pub(crate) struct AdminFormData { is_admin: String, } /// POST /admin/user/users/{id}/admin - Concede o revoca el acceso irrestricto (`is_admin`). /// /// No pasa por `require_permission`: conceder o revocar este flag no es un permiso del catálogo, /// se comprueba directamente contra `account.is_admin()` para que sólo un administrador pueda /// tocarlo (un permiso concedido vía rol nunca basta). pub(crate) async fn admin_post( request: HttpRequest, web::Path(id): web::Path, web::Query(waypoint): web::Query, web::Form(form): web::Form, ) -> Result { let Some(account) = request.extension::().cloned() else { return Err(ErrorPage::AccessDenied(Some(request))); }; if !account.is_admin() { return Err(ErrorPage::AccessDenied(Some(request))); } let new_is_admin = form.is_admin == "true"; // El botón envía la petición vía `hx-post` (para el diálogo `hx-confirm`), sin `hx-target`: si // se responde con un `Redirect` normal, HTMX sigue la redirección con `fetch` y sustituye el // propio `` (destino por defecto sin `hx-target`) por la página completa que devuelve, // anidándola dentro de sí misma. `HtmxResponse::redirect()` evita eso: instruye al cliente // (cabecera `HX-Redirect`) para que navegue de verdad a la URL, en vez de intentar un `swap`. let is_htmx = request.is_htmx(); match user_admin::set_user_admin(id, new_is_admin, account.id()).await { Ok(()) => { let cx = Context::admin(request); let edit_href = waypoint.append_to(cx.route(user_path(id, "edit"))); if is_htmx { Ok(HtmxResponse::empty().redirect(edit_href).into_response()) } else { Ok(Redirect::see_other(edit_href).into_response()) } } Err(err) => Ok(render_user_edit(request, id, Some(map_auth_error(&err)), waypoint).await), } } // **< password_get / password_post >*************************************************************** /// GET /admin/user/users/{id}/password - Formulario de restablecimiento de contraseña por un /// administrador. pub(crate) async fn password_get( request: HttpRequest, web::Path(id): web::Path, web::Query(waypoint): web::Query, ) -> Result { require_permission(&request, &UserPermission::AdminUsers)?; let user = require_manageable(&request, id).await?; let mut page = Page::admin(request); let edit_href = waypoint.append_to(page.context().route(user_path(id, "edit"))); let title = Lc::t("title-admin-user-password", &LOCALES_USER); Ok(page .with_title(title.clone()) .with_child( frame(title) .with_child(user_summary(&user)) .with_child( AdminPasswordForm::new() .with_user_id(id) .with_waypoint(waypoint), ) .with_child(back_link(edit_href)), ) .render() .await .into_response()) } #[derive(Deserialize)] pub(crate) struct AdminPasswordFormData { password: String, confirm_password: String, } /// POST /admin/user/users/{id}/password - Aplica la nueva contraseña e invalida las sesiones /// activas del usuario. pub(crate) async fn password_post( request: HttpRequest, web::Path(id): web::Path, web::Query(waypoint): web::Query, web::Form(form): web::Form, ) -> Result { require_permission(&request, &UserPermission::AdminUsers)?; let user = require_manageable(&request, id).await?; let result = match password::passwords_match(&form.password, &form.confirm_password) { Ok(()) => user_admin::admin_reset_password(id, &form.password).await, Err(err) => Err(err), }; match result { Ok(()) => { let cx = Context::admin(request); let edit_href = waypoint.append_to(cx.route(user_path(id, "edit"))); Ok(Redirect::see_other(edit_href).into_response()) } Err(err) => { let mut page = Page::admin(request); let edit_href = waypoint.append_to(page.context().route(user_path(id, "edit"))); let title = Lc::t("title-admin-user-password", &LOCALES_USER); Ok(page .with_title(title.clone()) .with_child( frame(title) .with_child(user_summary(&user)) .with_child( AdminPasswordForm::new() .with_user_id(id) .with_error(map_auth_error(&err)) .with_waypoint(waypoint), ) .with_child(back_link(edit_href)), ) .render() .await .into_response()) } } }