diff --git a/extensions/pagetop-admin/src/entity/setting.rs b/extensions/pagetop-admin/src/entity/setting.rs index 95b6c4c6..8907f0fd 100644 --- a/extensions/pagetop-admin/src/entity/setting.rs +++ b/extensions/pagetop-admin/src/entity/setting.rs @@ -1,4 +1,3 @@ -use pagetop::prelude::*; use pagetop_seaorm::db::*; /// Entidad SeaORM para la tabla `settings`. @@ -9,7 +8,7 @@ pub struct Model { pub key: String, pub scope: String, pub value: String, - pub updated_at: NaiveDateTime, + pub updated_at: DateTimeUtc, pub updated_by: Option, } diff --git a/extensions/pagetop-admin/src/settings.rs b/extensions/pagetop-admin/src/settings.rs index 60f73637..d145709d 100644 --- a/extensions/pagetop-admin/src/settings.rs +++ b/extensions/pagetop-admin/src/settings.rs @@ -11,7 +11,7 @@ use serde::{Serialize, de::DeserializeOwned}; use crate::entity::setting::{ActiveModel, Column, Entity}; use crate::error::AdminError; -// **< API pública >********************************************************************************* +// **< API pública >******************************************************************************** /// Lee un valor persistido, devolviendo el `Default` del tipo si no existe. pub async fn get(key: &str) -> T { @@ -40,7 +40,7 @@ pub async fn list_scope(scope: &str) -> Vec<(String, String)> { list_scope_async(scope).await.unwrap_or_default() } -// **< Implementación asíncrona >******************************************************************** +// **< Implementación asíncrona >******************************************************************* async fn get_async(key: &str) -> Result { let model = Entity::find_by_id(key) @@ -57,7 +57,7 @@ async fn set_async( user_id: Option, ) -> Result<(), AdminError> { let value_json = serde_json::to_string(value)?; - let now = Utc::now().naive_utc(); + let now = Utc::now(); let existing = Entity::find_by_id(key).one(dbconn()).await?; if existing.is_some() { @@ -95,7 +95,7 @@ async fn list_scope_async(scope: &str) -> Result, AdminErr Ok(rows.into_iter().map(|m| (m.key, m.value)).collect()) } -// **< Tipos de esquema >**************************************************************************** +// **< Tipos de esquema >*************************************************************************** /// Tipo de campo de configuración para un [`SettingsSchema`]. #[derive(Clone, Debug)] diff --git a/extensions/pagetop-user/src/auth.rs b/extensions/pagetop-user/src/auth.rs index 67d21aa6..6d737bda 100644 --- a/extensions/pagetop-user/src/auth.rs +++ b/extensions/pagetop-user/src/auth.rs @@ -24,7 +24,7 @@ pub async fn login( plain_password: &str, remember: bool, ) -> Result { - let now = Utc::now().naive_utc(); + let now = Utc::now(); // Buscar usuario por username o email. let user_model = user::Entity::find() @@ -119,7 +119,7 @@ pub async fn register( } let hash = password::hash_password(plain_password)?; - let now = Utc::now().naive_utc(); + let now = Utc::now(); let status = if SETTINGS.require_email_verification { UserStatus::Pending } else { @@ -175,7 +175,7 @@ pub async fn assign_role(user_id: i32, role_id: i32) -> Result<(), AuthError> { async fn register_failed_login( user_model: &user::Model, - now: NaiveDateTime, + now: DateTime, ) -> Result<(), AuthError> { let new_count = user_model.failed_login_count + 1; let lock_at = if new_count >= SETTINGS.max_failed_logins { @@ -236,7 +236,7 @@ async fn do_seed() { } }; - let now = Utc::now().naive_utc(); + let now = Utc::now(); let new_admin = user::ActiveModel { id: ActiveValue::NotSet, username: Set(cfg.admin_username.clone()), diff --git a/extensions/pagetop-user/src/component/admin/user_form.rs b/extensions/pagetop-user/src/component/admin/user_form.rs index 621f5d9e..42ccd0e2 100644 --- a/extensions/pagetop-user/src/component/admin/user_form.rs +++ b/extensions/pagetop-user/src/component/admin/user_form.rs @@ -1,5 +1,8 @@ //! Formulario de alta/edición de usuario. +use std::collections::BTreeMap; +use std::sync::LazyLock; + use pagetop::prelude::*; use crate::ADMIN_USERS_PATH; @@ -10,6 +13,32 @@ use crate::component::{PasswordConfirm, error_banner}; use super::{USER_ADMIN_FORM_ID, roles_fieldset}; +// Regiones de la base IANA que sólo contienen alias heredados (fichero `backward`), todos con una +// zona canónica equivalente en otra región (p. ej. `US/Eastern` es `America/New_York`). +const LEGACY_REGIONS: [&str; 5] = ["Brazil", "Canada", "Chile", "Mexico", "US"]; + +// Zonas horarias IANA canónicas agrupadas por región (lo anterior a la primera `/`), ordenadas por +// región y nombre. Se descartan los alias heredados: los nombres sin región (`GB`, `Japan`, +// `EST5EDT`...), los de `LEGACY_REGIONS` y los de `Etc` salvo `Etc/UTC`, cuyo grupo va al final. +static TZ_BY_REGION: LazyLock)>> = LazyLock::new(|| { + let mut regions: BTreeMap<&'static str, Vec<&'static str>> = BTreeMap::new(); + for tz in TZ_VARIANTS.iter() { + let name = tz.name(); + let Some((region, _)) = name.split_once('/') else { + continue; + }; + if LEGACY_REGIONS.contains(®ion) || (region == "Etc" && name != "Etc/UTC") { + continue; + } + regions.entry(region).or_default().push(name); + } + for names in regions.values_mut() { + names.sort_unstable(); + } + let etc = regions.remove_entry("Etc"); + regions.into_iter().chain(etc).collect() +}); + #[derive(AutoDefault, Clone, Copy, Debug, PartialEq)] pub(crate) enum UserFormMode { #[default] @@ -81,17 +110,13 @@ impl Component for UserForm { .with_value(self.language()) .with_label(Lc::t("field-language", &LOCALES_USER)), ) - .with_child( - form::input::Field::text() - .with_name("timezone") - .with_value(self.timezone()) - .with_label(Lc::t("field-timezone", &LOCALES_USER)), - ); + .with_child(timezone_field(self.timezone())); if *self.mode() == UserFormMode::New { - form = form - .with_child(PasswordConfirm::new()) - .with_child(roles_fieldset(self.roles())); + form = form.with_child(PasswordConfirm::new()); + if !self.roles().is_empty() { + form = form.with_child(roles_fieldset(self.roles())); + } if *self.allow_admin_field() { form = form.with_child( @@ -181,3 +206,29 @@ impl UserForm { self } } + +// `