♻️ (minimal): Reimplementa join! y join_pair!
Usa `[&str]::concat()` de la biblioteca estándar y elimina la dependencia de `concat-string`. Cada argumento se evalúa una sola vez y no se consume.
This commit is contained in:
parent
c6cd4fcd7d
commit
8651b91ab2
19 changed files with 105 additions and 108 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
|
@ -396,12 +396,6 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "concat-string"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7439becb5fafc780b6f4de382b1a7a3e70234afe783854a4702ee8adbb838609"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "concurrent-queue"
|
name = "concurrent-queue"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -1892,7 +1886,6 @@ dependencies = [
|
||||||
name = "pagetop-minimal"
|
name = "pagetop-minimal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"concat-string",
|
|
||||||
"indoc",
|
"indoc",
|
||||||
"pastey",
|
"pastey",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,6 @@ base64ct = { version = "1.8", features = ["alloc"] }
|
||||||
change-detection = "1.2"
|
change-detection = "1.2"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
colored = "3.1"
|
colored = "3.1"
|
||||||
concat-string = "1.0"
|
|
||||||
config = { version = "0.15", default-features = false, features = ["toml"] }
|
config = { version = "0.15", default-features = false, features = ["toml"] }
|
||||||
figlet-rs = "1.0"
|
figlet-rs = "1.0"
|
||||||
fluent-templates = "0.14"
|
fluent-templates = "0.14"
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! Proporciona una API async para leer y escribir valores JSON en la tabla `settings`.
|
//! Proporciona una API async para leer y escribir valores JSON en la tabla `settings`.
|
||||||
|
|
||||||
use pagetop::datetime::Utc;
|
use pagetop::prelude::*;
|
||||||
use pagetop::{Getters, builder_impl};
|
|
||||||
use pagetop_seaorm::db::{
|
use pagetop_seaorm::db::{
|
||||||
ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, dbconn,
|
ActiveModelTrait, ActiveValue, ColumnTrait, EntityTrait, QueryFilter, dbconn,
|
||||||
};
|
};
|
||||||
|
|
@ -231,6 +230,6 @@ impl SettingsSchema {
|
||||||
|
|
||||||
/// Devuelve la clave completa de un campo: `"{scope}.{field_name}"`.
|
/// Devuelve la clave completa de un campo: `"{scope}.{field_name}"`.
|
||||||
pub fn key_for(&self, field_name: &str) -> String {
|
pub fn key_for(&self, field_name: &str) -> String {
|
||||||
format!("{}.{}", self.scope, field_name)
|
util::join!(&self.scope, ".", field_name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -202,7 +202,7 @@ impl MenuPosition {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(crate) fn data_offset(self) -> Option<String> {
|
pub(crate) fn data_offset(self) -> Option<String> {
|
||||||
match self {
|
match self {
|
||||||
Self::Offset(x, y) => Some(format!("{x},{y}")),
|
Self::Offset(x, y) => Some(util::join!(x.to_string(), ",", y.to_string())),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -81,6 +81,8 @@
|
||||||
//! .with_prop(PropsOp::set(hx::on_htmx("after-swap"), "console.log('done')"));
|
//! .with_prop(PropsOp::set(hx::on_htmx("after-swap"), "console.log('done')"));
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
// **< HTTP Methods >*******************************************************************************
|
// **< HTTP Methods >*******************************************************************************
|
||||||
|
|
||||||
/// Realiza una petición GET al servidor y aplica la respuesta al objetivo.
|
/// Realiza una petición GET al servidor y aplica la respuesta al objetivo.
|
||||||
|
|
@ -356,7 +358,7 @@ pub const DISABLE: &str = "hx-disable";
|
||||||
/// .with_prop(PropsOp::set(hx::on("mouseenter"), "this.style.opacity='0.8'"));
|
/// .with_prop(PropsOp::set(hx::on("mouseenter"), "this.style.opacity='0.8'"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn on(event: &str) -> String {
|
pub fn on(event: &str) -> String {
|
||||||
format!("hx-on:{event}")
|
util::join!("hx-on:", event)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Genera `hx-on::{event}` para escuchar eventos propios de HTMX en línea.
|
/// Genera `hx-on::{event}` para escuchar eventos propios de HTMX en línea.
|
||||||
|
|
@ -372,7 +374,7 @@ pub fn on(event: &str) -> String {
|
||||||
/// .with_prop(PropsOp::set(hx::on_htmx("after-swap"), "initTooltips()"));
|
/// .with_prop(PropsOp::set(hx::on_htmx("after-swap"), "initTooltips()"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn on_htmx(event: &str) -> String {
|
pub fn on_htmx(event: &str) -> String {
|
||||||
format!("hx-on::{event}")
|
util::join!("hx-on::", event)
|
||||||
}
|
}
|
||||||
|
|
||||||
// **< HTMX Request Headers >***********************************************************************
|
// **< HTMX Request Headers >***********************************************************************
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::ADMIN_USERS_PATH;
|
|
||||||
use crate::LOCALES_USER;
|
use crate::LOCALES_USER;
|
||||||
|
use crate::user_path;
|
||||||
|
|
||||||
use crate::component::{PasswordConfirm, error_banner};
|
use crate::component::{PasswordConfirm, error_banner};
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ impl Component for AdminPasswordForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let action = format!("{ADMIN_USERS_PATH}/{}/password", self.user_id());
|
let action = user_path(self.user_id(), "password");
|
||||||
let action = self.waypoint().append_to(cx.route(action));
|
let action = self.waypoint().append_to(cx.route(action));
|
||||||
|
|
||||||
let mut form = Form::new()
|
let mut form = Form::new()
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::ADMIN_ROLES_PATH;
|
use crate::ADMIN_ROLES_PATH;
|
||||||
use crate::LOCALES_USER;
|
use crate::LOCALES_USER;
|
||||||
|
use crate::role_path;
|
||||||
|
|
||||||
use crate::component::error_banner;
|
use crate::component::error_banner;
|
||||||
|
|
||||||
|
|
@ -35,13 +36,8 @@ impl Component for RoleForm {
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let action = match self.mode() {
|
let action = match self.mode() {
|
||||||
RoleFormMode::New => format!("{ADMIN_ROLES_PATH}/new"),
|
RoleFormMode::New => util::join!(ADMIN_ROLES_PATH, "/new"),
|
||||||
RoleFormMode::Edit => {
|
RoleFormMode::Edit => role_path(self.role_id().copied().unwrap_or_default(), "edit"),
|
||||||
format!(
|
|
||||||
"{ADMIN_ROLES_PATH}/{}/edit",
|
|
||||||
self.role_id().copied().unwrap_or_default()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let action = self.waypoint().append_to(cx.route(action));
|
let action = self.waypoint().append_to(cx.route(action));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::ADMIN_ROLES_PATH;
|
|
||||||
use crate::LOCALES_USER;
|
use crate::LOCALES_USER;
|
||||||
|
use crate::role_path;
|
||||||
|
|
||||||
use crate::component::admin::PermissionGroups;
|
use crate::component::admin::PermissionGroups;
|
||||||
use crate::component::error_banner;
|
use crate::component::error_banner;
|
||||||
|
|
@ -26,7 +26,7 @@ impl Component for RolePermissionsForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let action = format!("{ADMIN_ROLES_PATH}/{}/permissions", self.role_id());
|
let action = role_path(self.role_id(), "permissions");
|
||||||
let action = self.waypoint().append_to(cx.route(action));
|
let action = self.waypoint().append_to(cx.route(action));
|
||||||
|
|
||||||
let mut form = Form::new()
|
let mut form = Form::new()
|
||||||
|
|
@ -37,12 +37,12 @@ impl Component for RolePermissionsForm {
|
||||||
|
|
||||||
for (idx, (group_label, perms)) in self.groups().iter().enumerate() {
|
for (idx, (group_label, perms)) in self.groups().iter().enumerate() {
|
||||||
let mut field = form::check::Field::new()
|
let mut field = form::check::Field::new()
|
||||||
.with_id(format!("permission-group-{idx}"))
|
.with_id(util::join!("permission-group-", idx.to_string()))
|
||||||
.with_name("permission_keys");
|
.with_name("permission_keys");
|
||||||
for (key, label, checked) in perms {
|
for (key, label, checked) in perms {
|
||||||
let text = label.lookup(cx).unwrap_or_default();
|
let text = label.lookup(cx).unwrap_or_default();
|
||||||
field = field.with_item(
|
field = field.with_item(
|
||||||
form::check::Item::new(key, Lc::n(format!("{text} ({key})")))
|
form::check::Item::new(key, Lc::n(util::join!(text, " (", key, ")")))
|
||||||
.with_checked(*checked),
|
.with_checked(*checked),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use pagetop_htmx::hx_table::sort_link;
|
||||||
|
|
||||||
use crate::ADMIN_ROLES_PATH;
|
use crate::ADMIN_ROLES_PATH;
|
||||||
use crate::LOCALES_USER;
|
use crate::LOCALES_USER;
|
||||||
|
use crate::role_path;
|
||||||
use crate::service::role_admin::{RoleListItem, RoleSortField};
|
use crate::service::role_admin::{RoleListItem, RoleSortField};
|
||||||
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
|
|
@ -108,7 +109,7 @@ impl Component for RoleTable {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_href = waypoint.append_to(cx.route(format!("{ADMIN_ROLES_PATH}/new")));
|
let new_href = waypoint.append_to(cx.route(util::join!(ADMIN_ROLES_PATH, "/new")));
|
||||||
|
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
div (self.props().unpack(cx)) {
|
div (self.props().unpack(cx)) {
|
||||||
|
|
@ -212,7 +213,7 @@ fn label_cell(role: &RoleListItem, waypoint: &Waypoint) -> Html {
|
||||||
let id = role.id;
|
let id = role.id;
|
||||||
let waypoint = waypoint.clone();
|
let waypoint = waypoint.clone();
|
||||||
Html::with(move |cx| {
|
Html::with(move |cx| {
|
||||||
let view_href = waypoint.append_to(cx.route(format!("{ADMIN_ROLES_PATH}/{id}/view")));
|
let view_href = waypoint.append_to(cx.route(role_path(id, "view")));
|
||||||
html! {
|
html! {
|
||||||
a href=(view_href) { (label.as_str()) }
|
a href=(view_href) { (label.as_str()) }
|
||||||
}
|
}
|
||||||
|
|
@ -234,8 +235,7 @@ async fn actions_cell(
|
||||||
let locked = role.locked;
|
let locked = role.locked;
|
||||||
let waypoint = waypoint.clone();
|
let waypoint = waypoint.clone();
|
||||||
|
|
||||||
let permissions_href =
|
let permissions_href = waypoint.append_to(cx.route(role_path(id, "permissions")));
|
||||||
waypoint.append_to(cx.route(format!("{ADMIN_ROLES_PATH}/{id}/permissions")));
|
|
||||||
|
|
||||||
// Los botones se renderizan aquí, no dentro del `Html::with()` de abajo: necesitan pasar por
|
// Los botones se renderizan aquí, no dentro del `Html::with()` de abajo: necesitan pasar por
|
||||||
// su propio ciclo de renderizado (`.render().await`) para que el tema activo los estilice
|
// su propio ciclo de renderizado (`.render().await`) para que el tema activo los estilice
|
||||||
|
|
@ -253,7 +253,7 @@ async fn actions_cell(
|
||||||
let (edit_button, delete_button) = if locked {
|
let (edit_button, delete_button) = if locked {
|
||||||
(None, None)
|
(None, None)
|
||||||
} else {
|
} else {
|
||||||
let edit_href = waypoint.append_to(cx.route(format!("{ADMIN_ROLES_PATH}/{id}/edit")));
|
let edit_href = waypoint.append_to(cx.route(role_path(id, "edit")));
|
||||||
let edit_button = Button::anchor(Lc::t("btn-edit", &LOCALES_USER), edit_href)
|
let edit_button = Button::anchor(Lc::t("btn-edit", &LOCALES_USER), edit_href)
|
||||||
.with_style(button::Style::Solid(Intent::Primary))
|
.with_style(button::Style::Solid(Intent::Primary))
|
||||||
.with_size(button::Size::Small)
|
.with_size(button::Size::Small)
|
||||||
|
|
@ -263,7 +263,7 @@ async fn actions_cell(
|
||||||
// Viaja como query string para que, tanto si el borrado falla como si tiene éxito, la
|
// Viaja como query string para que, tanto si el borrado falla como si tiene éxito, la
|
||||||
// tabla vuelva a mostrarse en la misma página/orden en que estaba, en vez de reiniciarse.
|
// tabla vuelva a mostrarse en la misma página/orden en que estaba, en vez de reiniciarse.
|
||||||
let confirm_href = cx
|
let confirm_href = cx
|
||||||
.route(format!("{ADMIN_ROLES_PATH}/{id}/delete/confirm"))
|
.route(role_path(id, "delete/confirm"))
|
||||||
.alter_param("sort", sort.as_str())
|
.alter_param("sort", sort.as_str())
|
||||||
.alter_param("dir", dir)
|
.alter_param("dir", dir)
|
||||||
.alter_param("page", page.to_string())
|
.alter_param("page", page.to_string())
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::ADMIN_USERS_PATH;
|
use crate::ADMIN_USERS_PATH;
|
||||||
use crate::LOCALES_USER;
|
use crate::LOCALES_USER;
|
||||||
|
use crate::user_path;
|
||||||
|
|
||||||
use crate::component::{PasswordConfirm, error_banner};
|
use crate::component::{PasswordConfirm, error_banner};
|
||||||
|
|
||||||
|
|
@ -43,13 +44,8 @@ impl Component for UserForm {
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let action = match self.mode() {
|
let action = match self.mode() {
|
||||||
UserFormMode::New => format!("{ADMIN_USERS_PATH}/new"),
|
UserFormMode::New => util::join!(ADMIN_USERS_PATH, "/new"),
|
||||||
UserFormMode::Edit => {
|
UserFormMode::Edit => user_path(self.user_id().copied().unwrap_or_default(), "edit"),
|
||||||
format!(
|
|
||||||
"{ADMIN_USERS_PATH}/{}/edit",
|
|
||||||
self.user_id().copied().unwrap_or_default()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
let action = self.waypoint().append_to(cx.route(action));
|
let action = self.waypoint().append_to(cx.route(action));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
|
|
||||||
use crate::ADMIN_USERS_PATH;
|
|
||||||
use crate::LOCALES_USER;
|
use crate::LOCALES_USER;
|
||||||
|
use crate::user_path;
|
||||||
|
|
||||||
use crate::component::error_banner;
|
use crate::component::error_banner;
|
||||||
|
|
||||||
|
|
@ -25,7 +25,7 @@ impl Component for UserRolesForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
async fn prepare(&self, cx: &mut Context) -> Result<Markup, ComponentError> {
|
||||||
let action = format!("{ADMIN_USERS_PATH}/{}/roles", self.user_id());
|
let action = user_path(self.user_id(), "roles");
|
||||||
let action = self.waypoint().append_to(cx.route(action));
|
let action = self.waypoint().append_to(cx.route(action));
|
||||||
|
|
||||||
let mut form = Form::new()
|
let mut form = Form::new()
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use crate::LOCALES_USER;
|
||||||
use crate::account::UserStatus;
|
use crate::account::UserStatus;
|
||||||
use crate::permission::UserPermission;
|
use crate::permission::UserPermission;
|
||||||
use crate::service::user_admin::{UserListItem, UserSortField};
|
use crate::service::user_admin::{UserListItem, UserSortField};
|
||||||
|
use crate::user_path;
|
||||||
|
|
||||||
#[derive(AutoDefault, Clone, Debug, Getters)]
|
#[derive(AutoDefault, Clone, Debug, Getters)]
|
||||||
pub(crate) struct UserTable {
|
pub(crate) struct UserTable {
|
||||||
|
|
@ -84,7 +85,7 @@ impl Component for UserTable {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/new")));
|
let new_href = waypoint.append_to(cx.route(util::join!(ADMIN_USERS_PATH, "/new")));
|
||||||
|
|
||||||
Ok(html! {
|
Ok(html! {
|
||||||
div (self.props().unpack(cx)) {
|
div (self.props().unpack(cx)) {
|
||||||
|
|
@ -191,7 +192,7 @@ fn username_cell(user: &UserListItem, waypoint: &Waypoint) -> Html {
|
||||||
let id = user.id;
|
let id = user.id;
|
||||||
let waypoint = waypoint.clone();
|
let waypoint = waypoint.clone();
|
||||||
Html::with(move |cx| {
|
Html::with(move |cx| {
|
||||||
let view_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/view")));
|
let view_href = waypoint.append_to(cx.route(user_path(id, "view")));
|
||||||
html! {
|
html! {
|
||||||
a href=(view_href) { (username.as_str()) }
|
a href=(view_href) { (username.as_str()) }
|
||||||
}
|
}
|
||||||
|
|
@ -208,7 +209,7 @@ async fn actions_cell(
|
||||||
cx: &mut Context,
|
cx: &mut Context,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let id = user.id;
|
let id = user.id;
|
||||||
let edit_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/edit")));
|
let edit_href = waypoint.append_to(cx.route(user_path(id, "edit")));
|
||||||
|
|
||||||
// El botón se renderiza aquí, no dentro del `Html::with()` de abajo: necesita pasar por su
|
// El botón se renderiza aquí, no dentro del `Html::with()` de abajo: necesita pasar por su
|
||||||
// propio ciclo de renderizado (`.render().await`) para que el tema activo lo estilice igual
|
// propio ciclo de renderizado (`.render().await`) para que el tema activo lo estilice igual
|
||||||
|
|
@ -220,7 +221,7 @@ async fn actions_cell(
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let roles_button = if can_assign_roles {
|
let roles_button = if can_assign_roles {
|
||||||
let roles_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/roles")));
|
let roles_href = waypoint.append_to(cx.route(user_path(id, "roles")));
|
||||||
Some(
|
Some(
|
||||||
Button::anchor(Lc::t("btn-manage-roles", &LOCALES_USER), roles_href)
|
Button::anchor(Lc::t("btn-manage-roles", &LOCALES_USER), roles_href)
|
||||||
.with_style(button::Style::Solid(Intent::Neutral))
|
.with_style(button::Style::Solid(Intent::Neutral))
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ use crate::config::SETTINGS;
|
||||||
use crate::entity::role;
|
use crate::entity::role;
|
||||||
use crate::handlers::admin::{back_link, frame, map_auth_error};
|
use crate::handlers::admin::{back_link, frame, map_auth_error};
|
||||||
use crate::permission::{self, UserPermission};
|
use crate::permission::{self, UserPermission};
|
||||||
|
use crate::role_path;
|
||||||
use crate::service::role_admin::{self, RolePageParams, RoleSortField};
|
use crate::service::role_admin::{self, RolePageParams, RoleSortField};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -451,7 +452,7 @@ pub(crate) async fn delete_confirm_get(
|
||||||
|
|
||||||
let mut cx = Context::admin(request);
|
let mut cx = Context::admin(request);
|
||||||
let delete_href = cx
|
let delete_href = cx
|
||||||
.route(format!("{ADMIN_ROLES_PATH}/{id}/delete"))
|
.route(role_path(id, "delete"))
|
||||||
.alter_param(
|
.alter_param(
|
||||||
"sort",
|
"sort",
|
||||||
RoleSortField::from_query(query.sort.as_deref()).as_str(),
|
RoleSortField::from_query(query.sort.as_deref()).as_str(),
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ use pagetop::base::component::table::Row;
|
||||||
use pagetop::prelude::*;
|
use pagetop::prelude::*;
|
||||||
use pagetop_htmx::prelude::*;
|
use pagetop_htmx::prelude::*;
|
||||||
|
|
||||||
use crate::ADMIN_ROLES_PATH;
|
|
||||||
use crate::ADMIN_USERS_PATH;
|
use crate::ADMIN_USERS_PATH;
|
||||||
use crate::ANONYMOUS_ROLE_ID;
|
use crate::ANONYMOUS_ROLE_ID;
|
||||||
use crate::AUTHENTICATED_ROLE_ID;
|
use crate::AUTHENTICATED_ROLE_ID;
|
||||||
|
|
@ -22,8 +21,10 @@ use crate::error::AuthError;
|
||||||
use crate::handlers::admin::{back_link, frame, map_auth_error};
|
use crate::handlers::admin::{back_link, frame, map_auth_error};
|
||||||
use crate::password;
|
use crate::password;
|
||||||
use crate::permission::UserPermission;
|
use crate::permission::UserPermission;
|
||||||
|
use crate::role_path;
|
||||||
use crate::service::role_admin;
|
use crate::service::role_admin;
|
||||||
use crate::service::user_admin::{self, UserListParams, UserSortField};
|
use crate::service::user_admin::{self, UserListParams, UserSortField};
|
||||||
|
use crate::user_path;
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub(crate) struct UsersQuery {
|
pub(crate) struct UsersQuery {
|
||||||
|
|
@ -341,12 +342,10 @@ fn edit_actions(
|
||||||
("true", "btn-grant-admin", "confirm-grant-admin")
|
("true", "btn-grant-admin", "confirm-grant-admin")
|
||||||
};
|
};
|
||||||
|
|
||||||
let roles_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/roles")));
|
let roles_href = waypoint.append_to(cx.route(user_path(user_id, "roles")));
|
||||||
let password_href =
|
let password_href = waypoint.append_to(cx.route(user_path(user_id, "password")));
|
||||||
waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/password")));
|
let status_action = waypoint.append_to(cx.route(user_path(user_id, "status")));
|
||||||
let status_action =
|
let admin_action = waypoint.append_to(cx.route(user_path(user_id, "admin")));
|
||||||
waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/status")));
|
|
||||||
let admin_action = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{user_id}/admin")));
|
|
||||||
|
|
||||||
let mut status_form = Form::new()
|
let mut status_form = Form::new()
|
||||||
.with_action(status_action.clone())
|
.with_action(status_action.clone())
|
||||||
|
|
@ -593,9 +592,10 @@ async fn user_view_roles(roles: &[role::Model], cx: &mut Context) -> Block {
|
||||||
table.user-admin-table {
|
table.user-admin-table {
|
||||||
tbody {
|
tbody {
|
||||||
@for (id, machine_name, label, system_badge) in &items {
|
@for (id, machine_name, label, system_badge) in &items {
|
||||||
|
@let href = cx.route(role_path(*id, "view")).to_string();
|
||||||
tr {
|
tr {
|
||||||
td {
|
td {
|
||||||
a href=(cx.route(format!("{ADMIN_ROLES_PATH}/{id}/view")).to_string()) {
|
a href=(href) {
|
||||||
(label.as_str())
|
(label.as_str())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -744,7 +744,7 @@ pub(crate) async fn status_post(
|
||||||
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(()) => {
|
Ok(()) => {
|
||||||
let cx = Context::admin(request);
|
let cx = Context::admin(request);
|
||||||
let edit_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/edit")));
|
let edit_href = waypoint.append_to(cx.route(user_path(id, "edit")));
|
||||||
if is_htmx {
|
if is_htmx {
|
||||||
Ok(HtmxResponse::empty().redirect(edit_href).into_response())
|
Ok(HtmxResponse::empty().redirect(edit_href).into_response())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -791,7 +791,7 @@ pub(crate) async fn admin_post(
|
||||||
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(()) => {
|
Ok(()) => {
|
||||||
let cx = Context::admin(request);
|
let cx = Context::admin(request);
|
||||||
let edit_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/edit")));
|
let edit_href = waypoint.append_to(cx.route(user_path(id, "edit")));
|
||||||
if is_htmx {
|
if is_htmx {
|
||||||
Ok(HtmxResponse::empty().redirect(edit_href).into_response())
|
Ok(HtmxResponse::empty().redirect(edit_href).into_response())
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -816,10 +816,7 @@ pub(crate) async fn password_get(
|
||||||
return Err(ErrorPage::NotFound(Some(request)));
|
return Err(ErrorPage::NotFound(Some(request)));
|
||||||
}
|
}
|
||||||
let mut page = Page::admin(request);
|
let mut page = Page::admin(request);
|
||||||
let edit_href = waypoint.append_to(
|
let edit_href = waypoint.append_to(page.context().route(user_path(id, "edit")));
|
||||||
page.context()
|
|
||||||
.route(format!("{ADMIN_USERS_PATH}/{id}/edit")),
|
|
||||||
);
|
|
||||||
let title = Lc::t("title-admin-user-password", &LOCALES_USER);
|
let title = Lc::t("title-admin-user-password", &LOCALES_USER);
|
||||||
Ok(page
|
Ok(page
|
||||||
.with_title(title.clone())
|
.with_title(title.clone())
|
||||||
|
|
@ -861,15 +858,12 @@ pub(crate) async fn password_post(
|
||||||
match result {
|
match result {
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
let cx = Context::admin(request);
|
let cx = Context::admin(request);
|
||||||
let edit_href = waypoint.append_to(cx.route(format!("{ADMIN_USERS_PATH}/{id}/edit")));
|
let edit_href = waypoint.append_to(cx.route(user_path(id, "edit")));
|
||||||
Ok(Redirect::see_other(edit_href).into_response())
|
Ok(Redirect::see_other(edit_href).into_response())
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let mut page = Page::admin(request);
|
let mut page = Page::admin(request);
|
||||||
let edit_href = waypoint.append_to(
|
let edit_href = waypoint.append_to(page.context().route(user_path(id, "edit")));
|
||||||
page.context()
|
|
||||||
.route(format!("{ADMIN_USERS_PATH}/{id}/edit")),
|
|
||||||
);
|
|
||||||
let title = Lc::t("title-admin-user-password", &LOCALES_USER);
|
let title = Lc::t("title-admin-user-password", &LOCALES_USER);
|
||||||
Ok(page
|
Ok(page
|
||||||
.with_title(title.clone())
|
.with_title(title.clone())
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,16 @@ const ADMIN_ROLES_PATH: &str = "/admin/user/roles";
|
||||||
// Catálogo de permisos registrados, agrupado por extensión (solo lectura).
|
// Catálogo de permisos registrados, agrupado por extensión (solo lectura).
|
||||||
const ADMIN_PERMISSIONS_PATH: &str = "/admin/user/permissions";
|
const ADMIN_PERMISSIONS_PATH: &str = "/admin/user/permissions";
|
||||||
|
|
||||||
|
// Ruta de una acción sobre el usuario `id`: `{ADMIN_USERS_PATH}/{id}/{tail}`.
|
||||||
|
fn user_path(id: i32, tail: &str) -> String {
|
||||||
|
util::join!(ADMIN_USERS_PATH, "/", id.to_string(), "/", tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ruta de una acción sobre el rol `id`: `{ADMIN_ROLES_PATH}/{id}/{tail}`.
|
||||||
|
fn role_path(id: i32, tail: &str) -> String {
|
||||||
|
util::join!(ADMIN_ROLES_PATH, "/", id.to_string(), "/", tail)
|
||||||
|
}
|
||||||
|
|
||||||
// **< Registro en pagetop-admin (fijo) >***********************************************************
|
// **< Registro en pagetop-admin (fijo) >***********************************************************
|
||||||
|
|
||||||
// Registra las páginas de usuarios, roles y permisos en el portal de `pagetop-admin`, bajo la
|
// Registra las páginas de usuarios, roles y permisos en el portal de `pagetop-admin`, bajo la
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
//! Gestión de sesiones de usuario (creación, carga, destrucción).
|
//! Gestión de sesiones de usuario (creación, carga, destrucción).
|
||||||
|
|
||||||
use pagetop::auth::CurrentUser;
|
use pagetop::prelude::*;
|
||||||
use pagetop::datetime::{Duration, Utc};
|
|
||||||
use pagetop::web::http::{HeaderMap, header};
|
|
||||||
use pagetop_seaorm::db::{
|
use pagetop_seaorm::db::{
|
||||||
ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set, dbconn,
|
ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set, dbconn,
|
||||||
};
|
};
|
||||||
|
|
@ -35,32 +33,33 @@ fn bytes_to_hex(bytes: &[u8]) -> String {
|
||||||
|
|
||||||
/// Construye el valor de la cabecera `Set-Cookie` para la cookie de sesión.
|
/// Construye el valor de la cabecera `Set-Cookie` para la cookie de sesión.
|
||||||
pub fn build_cookie(sid: &str, remember: bool) -> String {
|
pub fn build_cookie(sid: &str, remember: bool) -> String {
|
||||||
let mut parts = vec![
|
let mut cookie = util::join!(
|
||||||
format!("{}={}", SETTINGS.session_cookie_name, sid),
|
&SETTINGS.session_cookie_name,
|
||||||
"HttpOnly".into(),
|
"=",
|
||||||
"SameSite=Lax".into(),
|
sid,
|
||||||
"Path=/".into(),
|
"; HttpOnly; SameSite=Lax; Path=/"
|
||||||
];
|
);
|
||||||
if SETTINGS.secure_cookie {
|
if SETTINGS.secure_cookie {
|
||||||
parts.push("Secure".into());
|
cookie.push_str("; Secure");
|
||||||
}
|
}
|
||||||
if remember {
|
if remember {
|
||||||
parts.push(format!("Max-Age={}", SETTINGS.session_ttl_secs));
|
cookie.push_str("; Max-Age=");
|
||||||
|
cookie.push_str(&SETTINGS.session_ttl_secs.to_string());
|
||||||
}
|
}
|
||||||
parts.join("; ")
|
cookie
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Construye la cookie de expiración (Max-Age=0) para borrar la sesión del navegador.
|
/// Construye la cookie de expiración (Max-Age=0) para borrar la sesión del navegador.
|
||||||
pub fn expiry_cookie() -> String {
|
pub fn expiry_cookie() -> String {
|
||||||
format!(
|
util::join!(
|
||||||
"{}=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0",
|
&SETTINGS.session_cookie_name,
|
||||||
SETTINGS.session_cookie_name
|
"=; HttpOnly; SameSite=Lax; Path=/; Max-Age=0"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extrae el session ID de las cabeceras HTTP de la petición, si existe.
|
/// Extrae el session ID de las cabeceras HTTP de la petición, si existe.
|
||||||
pub fn extract_sid(headers: Option<&HeaderMap>) -> Option<String> {
|
pub fn extract_sid(headers: Option<&web::http::HeaderMap>) -> Option<String> {
|
||||||
let cookie_str = headers?.get(header::COOKIE)?.to_str().ok()?;
|
let cookie_str = headers?.get(web::http::header::COOKIE)?.to_str().ok()?;
|
||||||
|
|
||||||
let name = SETTINGS.session_cookie_name.as_str();
|
let name = SETTINGS.session_cookie_name.as_str();
|
||||||
for part in cookie_str.split(';') {
|
for part in cookie_str.split(';') {
|
||||||
|
|
@ -78,7 +77,7 @@ pub fn extract_sid(headers: Option<&HeaderMap>) -> Option<String> {
|
||||||
///
|
///
|
||||||
/// Si no hay cookie o la sesión ha expirado, devuelve `(CurrentUser::Anonymous, None)`.
|
/// Si no hay cookie o la sesión ha expirado, devuelve `(CurrentUser::Anonymous, None)`.
|
||||||
/// Se llama desde el middleware de sesión, que es async.
|
/// Se llama desde el middleware de sesión, que es async.
|
||||||
pub async fn resolve_session(headers: &HeaderMap) -> (CurrentUser, Option<Account>) {
|
pub async fn resolve_session(headers: &web::http::HeaderMap) -> (CurrentUser, Option<Account>) {
|
||||||
let Some(sid) = extract_sid(Some(headers)) else {
|
let Some(sid) = extract_sid(Some(headers)) else {
|
||||||
return (CurrentUser::Anonymous, None);
|
return (CurrentUser::Anonymous, None);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,5 @@ license.workspace = true
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
concat-string.workspace = true
|
|
||||||
indoc.workspace = true
|
indoc.workspace = true
|
||||||
pastey.workspace = true
|
pastey.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -28,11 +28,6 @@ concatenación de cadenas y el uso rápido de colecciones clave-valor.
|
||||||
Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del
|
Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del
|
||||||
*crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay).
|
*crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay).
|
||||||
|
|
||||||
Las macros para la concatenación de cadenas **`join!`** y **`join_pair!`** se apoyan internamente en
|
|
||||||
el *crate* [concat-string](https://crates.io/crates/concat_string), desarrollado por
|
|
||||||
[FaultyRAM](https://crates.io/users/FaultyRAM), para evitar el formato de cadenas cuando la
|
|
||||||
eficiencia pueda ser relevante.
|
|
||||||
|
|
||||||
La macro para generar identificadores dinámicos **`paste!`** se reexporta del *crate*
|
La macro para generar identificadores dinámicos **`paste!`** se reexporta del *crate*
|
||||||
[pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular
|
[pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular
|
||||||
`paste!` de [David Tolnay](https://crates.io/users/dtolnay).
|
`paste!` de [David Tolnay](https://crates.io/users/dtolnay).
|
||||||
|
|
|
||||||
|
|
@ -29,11 +29,6 @@ concatenación de cadenas y el uso rápido de colecciones clave-valor.
|
||||||
Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del
|
Las macros para texto multilínea **`indoc!`**, **`formatdoc!`** y **`concatdoc!`** se reexportan del
|
||||||
*crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay).
|
*crate* [indoc](https://crates.io/crates/indoc) de [David Tolnay](https://crates.io/users/dtolnay).
|
||||||
|
|
||||||
Las macros para la concatenación de cadenas **`join!`** y **`join_pair!`** se apoyan internamente en
|
|
||||||
el *crate* [concat-string](https://crates.io/crates/concat_string), desarrollado por
|
|
||||||
[FaultyRAM](https://crates.io/users/FaultyRAM), para evitar el formato de cadenas cuando la
|
|
||||||
eficiencia pueda ser relevante.
|
|
||||||
|
|
||||||
La macro para generar identificadores dinámicos **`paste!`** se reexporta del *crate*
|
La macro para generar identificadores dinámicos **`paste!`** se reexporta del *crate*
|
||||||
[pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular
|
[pastey](https://crates.io/crates/pastey), una implementación avanzada y soportada del popular
|
||||||
`paste!` de [David Tolnay](https://crates.io/users/dtolnay).
|
`paste!` de [David Tolnay](https://crates.io/users/dtolnay).
|
||||||
|
|
@ -43,9 +38,6 @@ La macro para generar identificadores dinámicos **`paste!`** se reexporta del *
|
||||||
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico"
|
html_favicon_url = "https://git.cillero.es/manuelcillero/pagetop/raw/branch/main/assets/favicon.ico"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub use concat_string::concat_string;
|
|
||||||
|
|
||||||
pub use indoc::{concatdoc, formatdoc, indoc};
|
pub use indoc::{concatdoc, formatdoc, indoc};
|
||||||
|
|
||||||
/// Permite *pegar* tokens y generar identificadores a partir de otros.
|
/// Permite *pegar* tokens y generar identificadores a partir de otros.
|
||||||
|
|
@ -60,11 +52,17 @@ pub use pastey::paste;
|
||||||
// La documentación anterior se copia en `pagetop::util::paste!` porque el *crate* original no la
|
// La documentación anterior se copia en `pagetop::util::paste!` porque el *crate* original no la
|
||||||
// define y `pagetop` no la hereda automáticamente.
|
// define y `pagetop` no la hereda automáticamente.
|
||||||
|
|
||||||
/// Concatena eficientemente varios fragmentos en un [`String`].
|
/// Concatena varios fragmentos en un [`String`] reservando una sola vez la memoria del resultado.
|
||||||
///
|
///
|
||||||
/// Esta macro exporta [`concat_string!`](https://docs.rs/concat-string). Acepta cualquier número de
|
/// Acepta uno o más fragmentos que implementen [`AsRef<str>`] (literales, `&str`, [`String`],
|
||||||
/// fragmentos que implementen [`AsRef<str>`] y construye un [`String`] con el tamaño óptimo, de
|
/// `Cow<str>`...) y construye el resultado con la capacidad exacta, sin pasar por el formateo. Es
|
||||||
/// forma eficiente y evitando el uso de cadenas de formato que penalicen el rendimiento.
|
/// la forma preferida de componer texto a partir de fragmentos, y suele ser más rápida que
|
||||||
|
/// `format!`.
|
||||||
|
///
|
||||||
|
/// Cada expresión se evalúa **una sola vez**, por lo que admite llamadas como `n.to_string()` o
|
||||||
|
/// cierres con efectos secundarios. Los valores que no son cadenas (números, tipos `Display`) se
|
||||||
|
/// convierten antes con `to_string()`. Si hace falta formato real (`{:02}`, `{:?}`, `{:.2}`...),
|
||||||
|
/// usar `format!`.
|
||||||
///
|
///
|
||||||
/// # Ejemplo
|
/// # Ejemplo
|
||||||
///
|
///
|
||||||
|
|
@ -81,11 +79,26 @@ pub use pastey::paste;
|
||||||
/// // Un único fragmento devuelve el mismo valor.
|
/// // Un único fragmento devuelve el mismo valor.
|
||||||
/// let single_result = join!("Hello");
|
/// let single_result = join!("Hello");
|
||||||
/// assert_eq!(single_result, "Hello".to_string());
|
/// assert_eq!(single_result, "Hello".to_string());
|
||||||
|
///
|
||||||
|
/// // Los fragmentos pueden ser de tipos distintos; los números se convierten antes.
|
||||||
|
/// let name = String::from("item");
|
||||||
|
/// let result_mixed = join!(&name, "-", 7.to_string());
|
||||||
|
/// assert_eq!(result_mixed, "item-7".to_string());
|
||||||
|
///
|
||||||
|
/// // Cada expresión se evalúa una sola vez.
|
||||||
|
/// let mut calls = 0;
|
||||||
|
/// let mut next = || {
|
||||||
|
/// calls += 1;
|
||||||
|
/// calls.to_string()
|
||||||
|
/// };
|
||||||
|
/// let result_once = join!("n", next());
|
||||||
|
/// assert_eq!(result_once, "n1".to_string());
|
||||||
|
/// assert_eq!(calls, 1);
|
||||||
/// ```
|
/// ```
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! join {
|
macro_rules! join {
|
||||||
($($arg:expr),+) => {
|
($($arg:expr),+ $(,)?) => {
|
||||||
$crate::concat_string!($($arg),+)
|
[$(::core::convert::AsRef::<str>::as_ref(&$arg)),+].concat()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -126,15 +139,15 @@ macro_rules! join_pair {
|
||||||
let second_val = $second;
|
let second_val = $second;
|
||||||
let separator_val = $separator;
|
let separator_val = $separator;
|
||||||
|
|
||||||
let first = AsRef::<str>::as_ref(&first_val);
|
let first = ::core::convert::AsRef::<str>::as_ref(&first_val);
|
||||||
let second = AsRef::<str>::as_ref(&second_val);
|
let second = ::core::convert::AsRef::<str>::as_ref(&second_val);
|
||||||
let separator = if first.is_empty() || second.is_empty() {
|
let separator = if first.is_empty() || second.is_empty() {
|
||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
AsRef::<str>::as_ref(&separator_val)
|
::core::convert::AsRef::<str>::as_ref(&separator_val)
|
||||||
};
|
};
|
||||||
|
|
||||||
$crate::concat_string!(first, separator, second)
|
[first, separator, second].concat()
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue