✨ Añade pagetop-user, con autenticación y RBAC
Identidad de usuario, sesiones en base de datos, roles y permisos (RBAC) con "authenticated" implícito, contraseñas con Argon2id, tokens de un solo uso para verificación de email y restablecimiento de contraseña, y la UI de administración completa (usuarios, roles, permisos) sobre pagetop-admin. Incluye datos de demostración opcionales tras la feature `demo-data`.
This commit is contained in:
parent
043873a954
commit
def0513246
57 changed files with 7409 additions and 0 deletions
137
extensions/pagetop-user/src/handlers/account.rs
Normal file
137
extensions/pagetop-user/src/handlers/account.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
//! Handler HTTP para el perfil del propio usuario autenticado.
|
||||
|
||||
use pagetop::base::component::table::Row;
|
||||
use pagetop::prelude::*;
|
||||
|
||||
use crate::account::UserStatus;
|
||||
use crate::component::admin::status_key;
|
||||
use crate::entity::{role, user};
|
||||
use crate::service::user_admin;
|
||||
use crate::{LOCALES_USER, LOGIN_PATH, PROFILE_PATH};
|
||||
|
||||
// **< profile_get >********************************************************************************
|
||||
|
||||
/// GET /user - Perfil del usuario autenticado. Redirige al formulario de inicio de sesión si no hay
|
||||
/// sesión activa, conservando la URL de retorno.
|
||||
pub(crate) async fn profile_get(request: HttpRequest) -> Response {
|
||||
let cx = Context::new(request.clone());
|
||||
let Some(id) = cx.current_user().id() else {
|
||||
let target = cx.route(LOGIN_PATH).with_param("next", PROFILE_PATH);
|
||||
return Redirect::see_other(target).into_response();
|
||||
};
|
||||
|
||||
let user = match user_admin::find_user(id).await {
|
||||
Ok(user) => user,
|
||||
Err(_) => return ErrorPage::NotFound(Some(request)).into_response(),
|
||||
};
|
||||
let roles = match user_admin::user_roles(id).await {
|
||||
Ok(roles) => roles,
|
||||
Err(_) => return ErrorPage::InternalError(Some(request)).into_response(),
|
||||
};
|
||||
let status = UserStatus::from_i16(user.status);
|
||||
|
||||
let mut page = Page::new(request);
|
||||
let details_block = profile_details(&user, status, page.context()).await;
|
||||
let roles_block = profile_roles(&roles, page.context()).await;
|
||||
|
||||
page.with_title(Lc::t("title-profile", &LOCALES_USER))
|
||||
.with_child(details_block)
|
||||
.with_child(roles_block)
|
||||
.render()
|
||||
.await
|
||||
.into_response()
|
||||
}
|
||||
|
||||
// Bloque de sólo lectura con los datos de perfil del usuario autenticado.
|
||||
async fn profile_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(
|
||||
Row::new()
|
||||
.with_cell(Lc::t("field-username-admin", &LOCALES_USER))
|
||||
.with_cell(user.username.as_str()),
|
||||
)
|
||||
.with_row(
|
||||
Row::new()
|
||||
.with_cell(Lc::t("field-email", &LOCALES_USER))
|
||||
.with_cell(user.email.as_str()),
|
||||
)
|
||||
.with_row(
|
||||
Row::new()
|
||||
.with_cell(Lc::t("field-display-name", &LOCALES_USER))
|
||||
.with_cell(user.display_name.as_deref().unwrap_or("-")),
|
||||
)
|
||||
.with_row(
|
||||
Row::new()
|
||||
.with_cell(Lc::t("field-language", &LOCALES_USER))
|
||||
.with_cell(user.language.as_deref().unwrap_or("-")),
|
||||
)
|
||||
.with_row(
|
||||
Row::new()
|
||||
.with_cell(Lc::t("field-timezone", &LOCALES_USER))
|
||||
.with_cell(user.timezone.as_deref().unwrap_or("-")),
|
||||
)
|
||||
.with_row(
|
||||
Row::new()
|
||||
.with_cell(Lc::t("col-status", &LOCALES_USER))
|
||||
.with_cell(Lc::t(status_key(status), &LOCALES_USER)),
|
||||
);
|
||||
|
||||
if user.is_admin {
|
||||
let badge = Badge::labeled(Lc::t("badge-admin", &LOCALES_USER))
|
||||
.with_prop(PropsOp::add_classes("user-admin-badge-admin"))
|
||||
.render(cx)
|
||||
.await;
|
||||
table = table.with_row(
|
||||
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 del usuario autenticado. A diferencia de la vista de
|
||||
// administración, no enlaza cada rol a su pantalla de detalle: un usuario sin permisos de
|
||||
// administración no puede acceder a ella.
|
||||
async fn profile_roles(roles: &[role::Model], cx: &mut Context) -> Block {
|
||||
let mut items: Vec<(String, Option<Markup>)> = Vec::with_capacity(roles.len());
|
||||
for r in roles {
|
||||
let system_badge = if r.locked {
|
||||
Some(
|
||||
Badge::labeled(Lc::t("badge-system-role", &LOCALES_USER))
|
||||
.with_prop(PropsOp::add_classes("user-admin-badge-system"))
|
||||
.render(cx)
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
items.push((r.label.clone(), system_badge));
|
||||
}
|
||||
|
||||
Block::new()
|
||||
.with_title(Lc::t("field-roles", &LOCALES_USER))
|
||||
.with_child(Html::with(move |_cx| {
|
||||
html! {
|
||||
@if items.is_empty() {
|
||||
"-"
|
||||
} @else {
|
||||
ul.user-profile-roles {
|
||||
@for (label, system_badge) in &items {
|
||||
li {
|
||||
(label.as_str())
|
||||
@if let Some(badge) = system_badge {
|
||||
" "
|
||||
(badge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue