✨ (user): Asigna roles desde la edición de usuario
This commit is contained in:
parent
806ed9e0dc
commit
f819181872
26 changed files with 1219 additions and 544 deletions
|
|
@ -11,9 +11,9 @@ 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, UserRolesForm, UserTable,
|
||||
status_key,
|
||||
AdminPasswordForm, USER_ADMIN_FORM_ID, UserForm, UserFormMode, UserTable, status_key,
|
||||
};
|
||||
use crate::component::{language_name, multiline_text, theme_name};
|
||||
use crate::config::SETTINGS;
|
||||
use crate::entity::{role, user};
|
||||
use crate::error::AuthError;
|
||||
|
|
@ -112,11 +112,8 @@ fn search_bar(current_query: Option<String>) -> Html {
|
|||
// **< available_roles >****************************************************************************
|
||||
|
||||
// Roles asignables desde la UI de usuarios: excluye "anonymous" (nunca se asigna explícitamente)
|
||||
// y "authenticated" (implícito, nunca se asigna). `pub(crate)` porque también la usa
|
||||
// `component::admin::user_table` para decidir si hay algo que gestionar.
|
||||
pub(crate) async fn available_roles(
|
||||
selected: &[i32],
|
||||
) -> Result<Vec<(i32, String, bool)>, AuthError> {
|
||||
// y "authenticated" (implícito, nunca se asigna).
|
||||
async fn available_roles(selected: &[i32]) -> Result<Vec<(i32, String, bool)>, AuthError> {
|
||||
let items = role_admin::list_roles(&role_admin::RoleListParams {
|
||||
sort: role_admin::RoleSortField::Weight,
|
||||
dir: SortDir::Asc,
|
||||
|
|
@ -132,6 +129,27 @@ pub(crate) async fn available_roles(
|
|||
.collect())
|
||||
}
|
||||
|
||||
// Identificadores de rol enviados en el formulario de alta o edición.
|
||||
fn parse_role_ids(role_ids: &[String]) -> Vec<i32> {
|
||||
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<user::Model, ErrorPage> {
|
||||
let Ok(user) = user_admin::find_user(id).await else {
|
||||
return Err(ErrorPage::NotFound(Some(request.clone())));
|
||||
};
|
||||
if user.is_admin && !request.extension::<Account>().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.
|
||||
|
|
@ -146,7 +164,7 @@ pub(crate) async fn new_get(
|
|||
};
|
||||
// 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::<Account>().is_some_and(|a| a.is_admin);
|
||||
let allow_admin_field = request.extension::<Account>().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);
|
||||
|
|
@ -181,6 +199,8 @@ pub(crate) struct NewUserFormData {
|
|||
#[serde(default)]
|
||||
timezone: String,
|
||||
#[serde(default)]
|
||||
theme: String,
|
||||
#[serde(default)]
|
||||
role_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
is_admin: bool,
|
||||
|
|
@ -199,16 +219,11 @@ pub(crate) async fn new_post(
|
|||
let Ok(form) = serde_qs::from_bytes::<NewUserFormData>(&raw.0) else {
|
||||
return Err(ErrorPage::BadRequest(Some(request)));
|
||||
};
|
||||
let allow_admin_field = request.extension::<Account>().is_some_and(|a| a.is_admin);
|
||||
let allow_admin_field = request.extension::<Account>().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: Vec<i32> = form
|
||||
.role_ids
|
||||
.iter()
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
let role_ids = parse_role_ids(&form.role_ids);
|
||||
|
||||
let result = user_admin::create_user(user_admin::NewUserData {
|
||||
username: &form.username,
|
||||
|
|
@ -218,6 +233,7 @@ pub(crate) async fn new_post(
|
|||
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,
|
||||
})
|
||||
|
|
@ -240,6 +256,7 @@ pub(crate) async fn new_post(
|
|||
.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)
|
||||
|
|
@ -262,6 +279,7 @@ pub(crate) async fn new_post(
|
|||
|
||||
// **< 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,
|
||||
|
|
@ -272,26 +290,54 @@ async fn render_user_edit(
|
|||
Ok(user) => user,
|
||||
Err(_) => return ErrorPage::NotFound(Some(request)).into_response(),
|
||||
};
|
||||
let status = UserStatus::from_i16(user.status);
|
||||
// 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::<Account>()
|
||||
.is_some_and(|a| a.is_admin && a.id != id);
|
||||
let has_assignable_roles = match available_roles(&[]).await {
|
||||
Ok(roles) => !roles.is_empty(),
|
||||
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::<Account>()
|
||||
.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,
|
||||
user.is_admin,
|
||||
is_admin,
|
||||
can_toggle_admin,
|
||||
has_assignable_roles,
|
||||
&waypoint,
|
||||
page.context(),
|
||||
);
|
||||
|
|
@ -300,15 +346,8 @@ async fn render_user_edit(
|
|||
.with_child(
|
||||
frame(title)
|
||||
.with_child(
|
||||
UserForm::new()
|
||||
.with_mode(UserFormMode::Edit)
|
||||
form.with_mode(UserFormMode::Edit)
|
||||
.with_user_id(Some(id))
|
||||
.with_username(user.username)
|
||||
.with_email(user.email)
|
||||
.with_display_name(user.display_name.unwrap_or_default())
|
||||
.with_language(user.language.unwrap_or_default())
|
||||
.with_timezone(user.timezone.unwrap_or_default())
|
||||
.with_error(error)
|
||||
.with_waypoint(waypoint.clone()),
|
||||
)
|
||||
.with_child(actions)
|
||||
|
|
@ -319,24 +358,23 @@ async fn render_user_edit(
|
|||
.into_response()
|
||||
}
|
||||
|
||||
// Enlaces a las pantallas dedicadas (roles, 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ó.
|
||||
// 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 `<form>` de `UserForm` vía el atributo `form`, ver `USER_ADMIN_FORM_ID`),
|
||||
// "Gestionar roles" 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").
|
||||
// "Guardar" (envía el `<form>` 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,
|
||||
has_assignable_roles: bool,
|
||||
waypoint: &Waypoint,
|
||||
cx: &mut Context,
|
||||
) -> Flex {
|
||||
|
|
@ -350,7 +388,6 @@ fn edit_actions(
|
|||
("true", "btn-grant-admin", "confirm-grant-admin")
|
||||
};
|
||||
|
||||
let roles_href = waypoint.append_to(cx.route(user_path(user_id, "roles")));
|
||||
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")));
|
||||
|
|
@ -377,11 +414,6 @@ fn edit_actions(
|
|||
.with_style(button::Style::Solid(Intent::Primary))
|
||||
.with_prop(PropsOp::set("form", USER_ADMIN_FORM_ID)),
|
||||
)
|
||||
.with_child(
|
||||
Button::anchor(Lc::t("btn-manage-roles", &LOCALES_USER), roles_href)
|
||||
.with_style(button::Style::Solid(Intent::Neutral))
|
||||
.with_disabled(!has_assignable_roles),
|
||||
)
|
||||
.with_child(
|
||||
Button::anchor(Lc::t("btn-reset-password", &LOCALES_USER), password_href)
|
||||
.with_style(button::Style::Solid(Intent::Neutral)),
|
||||
|
|
@ -389,12 +421,14 @@ fn edit_actions(
|
|||
.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(Button::submit(Lc::t(admin_label_key, &LOCALES_USER)));
|
||||
.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));
|
||||
}
|
||||
|
|
@ -411,6 +445,7 @@ pub(crate) async fn edit_get(
|
|||
web::Query(waypoint): web::Query<Waypoint>,
|
||||
) -> Result<Response, ErrorPage> {
|
||||
require_permission(&request, &UserPermission::AdminUsers)?;
|
||||
require_manageable(&request, id).await?;
|
||||
Ok(render_user_edit(request, id, None, waypoint).await)
|
||||
}
|
||||
|
||||
|
|
@ -421,31 +456,49 @@ pub(crate) struct EditUserFormData {
|
|||
#[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<String>,
|
||||
}
|
||||
|
||||
/// POST /admin/user/users/{id}/edit - Actualiza los datos de perfil de un usuario.
|
||||
/// 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<i32>,
|
||||
web::Query(waypoint): web::Query<Waypoint>,
|
||||
web::Form(form): web::Form<EditUserFormData>,
|
||||
raw: web::RawForm,
|
||||
) -> Result<Response, ErrorPage> {
|
||||
require_permission(&request, &UserPermission::AdminUsers)?;
|
||||
let user = require_manageable(&request, id).await?;
|
||||
let Ok(form) = serde_qs::from_bytes::<EditUserFormData>(&raw.0) else {
|
||||
return Err(ErrorPage::BadRequest(Some(request)));
|
||||
};
|
||||
let role_ids = parse_role_ids(&form.role_ids);
|
||||
|
||||
let result = user_admin::update_user(
|
||||
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(()) => {
|
||||
|
|
@ -454,29 +507,19 @@ pub(crate) async fn edit_post(
|
|||
Ok(Redirect::see_other(target).into_response())
|
||||
}
|
||||
Err(err) => {
|
||||
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::Edit)
|
||||
.with_user_id(Some(id))
|
||||
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_error(map_auth_error(&err))
|
||||
.with_waypoint(waypoint);
|
||||
let title = Lc::t("title-admin-user-edit", &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())
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -505,7 +548,7 @@ pub(crate) async fn view_get(
|
|||
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, 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
|
||||
|
|
@ -540,20 +583,40 @@ async fn user_view_details(user: &user::Model, status: UserStatus, cx: &mut Cont
|
|||
.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()),
|
||||
)),
|
||||
)
|
||||
.with_row(
|
||||
table::Row::new()
|
||||
.with_cell(Lc::t("field-language", &LOCALES_USER))
|
||||
.with_cell(user.language.as_deref().unwrap_or("-")),
|
||||
.with_cell(language_name(user.language.as_deref())),
|
||||
)
|
||||
.with_row(
|
||||
table::Row::new()
|
||||
.with_cell(Lc::t("field-timezone", &LOCALES_USER))
|
||||
.with_cell(user.timezone.as_deref().unwrap_or("-")),
|
||||
)
|
||||
.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 {
|
||||
|
|
@ -574,10 +637,13 @@ async fn user_view_details(user: &user::Model, status: UserStatus, cx: &mut Cont
|
|||
|
||||
// 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], cx: &mut Context) -> Block {
|
||||
let mut table = Table::new()
|
||||
.with_prop(PropsOp::add_classes("user-admin-table"))
|
||||
.with_empty(Lc::n("-"));
|
||||
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 {
|
||||
|
|
@ -602,109 +668,32 @@ async fn user_view_roles(roles: &[role::Model], cx: &mut Context) -> Block {
|
|||
);
|
||||
}
|
||||
|
||||
Block::new()
|
||||
let mut block = Block::new()
|
||||
.with_title(Lc::t("field-roles", &LOCALES_USER))
|
||||
.with_child(table)
|
||||
.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
|
||||
}
|
||||
|
||||
// **< roles_get / roles_post >*********************************************************************
|
||||
|
||||
/// GET /admin/user/users/{id}/roles - Formulario de asignación de roles de un usuario.
|
||||
pub(crate) async fn roles_get(
|
||||
request: HttpRequest,
|
||||
web::Path(id): web::Path<i32>,
|
||||
web::Query(waypoint): web::Query<Waypoint>,
|
||||
) -> Result<Response, ErrorPage> {
|
||||
require_permission(&request, &UserPermission::AdminUsers)?;
|
||||
require_permission(&request, &UserPermission::AssignRoles)?;
|
||||
|
||||
if user_admin::find_user(id).await.is_err() {
|
||||
return Err(ErrorPage::NotFound(Some(request)));
|
||||
}
|
||||
let current = match user_admin::user_role_ids(id).await {
|
||||
Ok(ids) => ids,
|
||||
Err(_) => return Err(ErrorPage::InternalError(Some(request))),
|
||||
};
|
||||
let roles = match available_roles(¤t).await {
|
||||
Ok(roles) => roles,
|
||||
Err(_) => return Err(ErrorPage::InternalError(Some(request))),
|
||||
};
|
||||
|
||||
let mut page = Page::admin(request);
|
||||
let back_href = waypoint.or(page.context().route(ADMIN_USERS_PATH));
|
||||
|
||||
let title = Lc::t("title-admin-user-roles", &LOCALES_USER);
|
||||
Ok(page
|
||||
.with_title(title.clone())
|
||||
.with_child(
|
||||
frame(title)
|
||||
.with_child(
|
||||
UserRolesForm::new()
|
||||
.with_user_id(id)
|
||||
.with_roles(roles)
|
||||
.with_waypoint(waypoint),
|
||||
)
|
||||
.with_child(back_link(back_href)),
|
||||
// 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("-")),
|
||||
)
|
||||
.render()
|
||||
.await
|
||||
.into_response())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct UserRolesFormData {
|
||||
#[serde(default)]
|
||||
role_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// POST /admin/user/users/{id}/roles - Reemplaza el conjunto de roles asignados a un usuario.
|
||||
pub(crate) async fn roles_post(
|
||||
request: HttpRequest,
|
||||
web::Path(id): web::Path<i32>,
|
||||
web::Query(waypoint): web::Query<Waypoint>,
|
||||
raw: web::RawForm,
|
||||
) -> Result<Response, ErrorPage> {
|
||||
require_permission(&request, &UserPermission::AdminUsers)?;
|
||||
require_permission(&request, &UserPermission::AssignRoles)?;
|
||||
|
||||
let Ok(form) = serde_qs::from_bytes::<UserRolesFormData>(&raw.0) else {
|
||||
return Err(ErrorPage::BadRequest(Some(request)));
|
||||
};
|
||||
|
||||
let role_ids: Vec<i32> = form
|
||||
.role_ids
|
||||
.iter()
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
|
||||
match user_admin::set_user_roles(id, &role_ids).await {
|
||||
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 = UserRolesForm::new()
|
||||
.with_user_id(id)
|
||||
.with_roles(roles)
|
||||
.with_error(map_auth_error(&err))
|
||||
.with_waypoint(waypoint);
|
||||
let title = Lc::t("title-admin-user-roles", &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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **< status_post >********************************************************************************
|
||||
|
|
@ -723,6 +712,7 @@ pub(crate) async fn status_post(
|
|||
) -> Result<Response, ErrorPage> {
|
||||
require_permission(&request, &UserPermission::AdminUsers)?;
|
||||
require_permission(&request, &UserPermission::BlockAccounts)?;
|
||||
require_manageable(&request, id).await?;
|
||||
let Some(account) = request.extension::<Account>().cloned() else {
|
||||
return Err(ErrorPage::AccessDenied(Some(request)));
|
||||
};
|
||||
|
|
@ -736,7 +726,7 @@ pub(crate) async fn status_post(
|
|||
// del propio `<form>`. `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 {
|
||||
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")));
|
||||
|
|
@ -760,7 +750,7 @@ pub(crate) struct AdminFormData {
|
|||
/// 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
|
||||
/// 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,
|
||||
|
|
@ -771,7 +761,7 @@ pub(crate) async fn admin_post(
|
|||
let Some(account) = request.extension::<Account>().cloned() else {
|
||||
return Err(ErrorPage::AccessDenied(Some(request)));
|
||||
};
|
||||
if !account.is_admin {
|
||||
if !account.is_admin() {
|
||||
return Err(ErrorPage::AccessDenied(Some(request)));
|
||||
}
|
||||
|
||||
|
|
@ -783,7 +773,7 @@ pub(crate) async fn admin_post(
|
|||
// (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 {
|
||||
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")));
|
||||
|
|
@ -807,9 +797,7 @@ pub(crate) async fn password_get(
|
|||
web::Query(waypoint): web::Query<Waypoint>,
|
||||
) -> Result<Response, ErrorPage> {
|
||||
require_permission(&request, &UserPermission::AdminUsers)?;
|
||||
if user_admin::find_user(id).await.is_err() {
|
||||
return Err(ErrorPage::NotFound(Some(request)));
|
||||
}
|
||||
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);
|
||||
|
|
@ -817,6 +805,7 @@ pub(crate) async fn password_get(
|
|||
.with_title(title.clone())
|
||||
.with_child(
|
||||
frame(title)
|
||||
.with_child(user_summary(&user))
|
||||
.with_child(
|
||||
AdminPasswordForm::new()
|
||||
.with_user_id(id)
|
||||
|
|
@ -844,6 +833,7 @@ pub(crate) async fn password_post(
|
|||
web::Form(form): web::Form<AdminPasswordFormData>,
|
||||
) -> Result<Response, ErrorPage> {
|
||||
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,
|
||||
|
|
@ -864,6 +854,7 @@ pub(crate) async fn password_post(
|
|||
.with_title(title.clone())
|
||||
.with_child(
|
||||
frame(title)
|
||||
.with_child(user_summary(&user))
|
||||
.with_child(
|
||||
AdminPasswordForm::new()
|
||||
.with_user_id(id)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue