//! Gestión de sesiones de usuario (creación, carga, destrucción). use pagetop::prelude::*; use pagetop_seaorm::db::{ ActiveModelTrait, ColumnTrait, DbErr, EntityTrait, QueryFilter, Set, dbconn, }; use crate::AUTHENTICATED_ROLE_ID; use crate::account::{Account, PermissionSet, UserStatus}; use crate::config::SETTINGS; use crate::entity::{role, role_permission, session, user, user_role}; // **< Generación de session ID >******************************************************************* /// Genera un session ID de 64 caracteres hex (32 bytes aleatorios via OsRng). pub fn generate_sid() -> String { use rand_core::{OsRng, RngCore}; let mut bytes = [0u8; 32]; OsRng.fill_bytes(&mut bytes); bytes_to_hex(&bytes) } fn bytes_to_hex(bytes: &[u8]) -> String { use std::fmt::Write; let mut s = String::with_capacity(bytes.len() * 2); for b in bytes { write!(s, "{:02x}", b).unwrap(); } s } // **< Cookie helpers >***************************************************************************** /// Construye el valor de la cabecera `Set-Cookie` para la cookie de sesión. pub fn build_cookie(sid: &str, remember: bool) -> String { let mut cookie = util::join!( &SETTINGS.session_cookie_name, "=", sid, "; HttpOnly; SameSite=Lax; Path=/" ); if SETTINGS.secure_cookie { cookie.push_str("; Secure"); } if remember { cookie.push_str("; Max-Age="); cookie.push_str(&SETTINGS.session_ttl_secs.to_string()); } cookie } /// Construye la cookie de expiración (Max-Age=0) para borrar la sesión del navegador. pub fn expiry_cookie() -> String { util::join!( &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. pub fn extract_sid(headers: Option<&web::http::HeaderMap>) -> Option { let cookie_str = headers?.get(web::http::header::COOKIE)?.to_str().ok()?; let name = SETTINGS.session_cookie_name.as_str(); for part in cookie_str.split(';') { let part = part.trim(); if let Some(value) = part.strip_prefix(name).and_then(|s| s.strip_prefix('=')) { return Some(value.trim().to_owned()); } } None } // **< resolve_session >**************************************************************************** /// Lee la cookie de sesión de las cabeceras y resuelve el par `(CurrentUser, Option)`. /// /// 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. pub async fn resolve_session(headers: &web::http::HeaderMap) -> (CurrentUser, Option) { let Some(sid) = extract_sid(Some(headers)) else { return (CurrentUser::Anonymous, None); }; load_user_from_session(&sid).await } // **< load_user_from_session >********************************************************************* /// Carga el par `(CurrentUser, Option)` a partir de un session ID. /// /// Devuelve `(CurrentUser::Anonymous, None)` si la sesión no existe o ha expirado. pub async fn load_user_from_session(sid: &str) -> (CurrentUser, Option) { let now = Utc::now().naive_utc(); // Buscar sesión activa y no expirada. let Ok(Some(sess)) = session::Entity::find_by_id(sid).one(dbconn()).await else { return (CurrentUser::Anonymous, None); }; if sess.expires_at < now { return (CurrentUser::Anonymous, None); } // Cargar usuario con estado activo. let Ok(Some(user_model)) = user::Entity::find_by_id(sess.user_id).one(dbconn()).await else { return (CurrentUser::Anonymous, None); }; if UserStatus::from_i16(user_model.status) != UserStatus::Active { return (CurrentUser::Anonymous, None); } // Cargar roles explícitos del usuario. let Ok(user_role_rows) = user_role::Entity::find() .filter(user_role::Column::UserId.eq(sess.user_id)) .all(dbconn()) .await else { return (CurrentUser::Anonymous, None); }; let role_ids: Vec = user_role_rows.iter().map(|ur| ur.role_id).collect(); let Ok(role_rows) = role::Entity::find() .filter(role::Column::Id.is_in(role_ids.clone())) .all(dbconn()) .await else { return (CurrentUser::Anonymous, None); }; let is_admin = user_model.is_admin; let role_names: Vec = role_rows.iter().map(|r| r.machine_name.clone()).collect(); // Cargar permisos de todos los roles más los del rol implícito "authenticated", que no se // asigna en `user_role` pero se aplica siempre. let mut all_role_ids = role_ids; if !all_role_ids.contains(&AUTHENTICATED_ROLE_ID) { all_role_ids.push(AUTHENTICATED_ROLE_ID); } let permissions = if is_admin { PermissionSet::default() } else { let Ok(perm_rows) = role_permission::Entity::find() .filter(role_permission::Column::RoleId.is_in(all_role_ids)) .all(dbconn()) .await else { return (CurrentUser::Anonymous, None); }; PermissionSet::new(perm_rows.into_iter().map(|p| p.permission_key)) }; // Actualizar last_activity_at con throttle: sólo una vez por minuto. let throttle = Duration::minutes(1); if sess .last_activity_at .map(|t| now - t > throttle) .unwrap_or(true) { let mut active: session::ActiveModel = sess.into(); active.last_activity_at = Set(Some(now)); let _ = active.update(dbconn()).await; } let display_name = user_model.display_name.unwrap_or_default(); let visible_name = if display_name.is_empty() { user_model.username.clone() } else { display_name.clone() }; let account = Account { id: user_model.id, username: user_model.username, email: user_model.email, display_name, status: UserStatus::from_i16(user_model.status), roles: role_names, permissions, is_admin, }; let current_user = CurrentUser::Authenticated { id: account.id, display_name: visible_name, }; (current_user, Some(account)) } // **< create_session >***************************************************************************** /// Crea una nueva sesión en base de datos y devuelve el session ID. pub async fn create_session(user_id: i32, remember: bool) -> Result { let sid = generate_sid(); let now = Utc::now().naive_utc(); let ttl = Duration::seconds(SETTINGS.session_ttl_secs); let idle = Duration::seconds(SETTINGS.session_idle_ttl_secs); let expires_at = if remember { now + ttl } else { now + idle }; let new_session = session::ActiveModel { sid: Set(sid.clone()), user_id: Set(user_id), data: Set("{}".into()), last_activity_at: Set(Some(now)), expires_at: Set(expires_at), created_at: Set(now), }; session::Entity::insert(new_session).exec(dbconn()).await?; Ok(sid) } /// Destruye la sesión indicada (logout). pub async fn destroy_session(sid: &str) -> Result<(), DbErr> { session::Entity::delete_by_id(sid).exec(dbconn()).await?; Ok(()) } /// Destruye todas las sesiones de un usuario (p. ej. al cambiar contraseña). pub async fn destroy_user_sessions(user_id: i32) -> Result<(), DbErr> { session::Entity::delete_many() .filter(session::Column::UserId.eq(user_id)) .exec(dbconn()) .await?; Ok(()) }