339 lines
11 KiB
Rust
339 lines
11 KiB
Rust
//! Lógica de autenticación: login, logout, registro, cambio de contraseña, semilla inicial.
|
|
|
|
use pagetop::prelude::*;
|
|
use pagetop_seaorm::db::{
|
|
ActiveModelTrait, ActiveValue, ColumnTrait, Condition, EntityTrait, PaginatorTrait,
|
|
QueryFilter, Set, dbconn,
|
|
};
|
|
|
|
use crate::account::UserStatus;
|
|
use crate::config::SETTINGS;
|
|
use crate::entity::{role_permission, user, user_role};
|
|
use crate::error::AuthError;
|
|
use crate::password;
|
|
use crate::permission::UserPermission;
|
|
use crate::session;
|
|
|
|
// **< login >**************************************************************************************
|
|
|
|
/// Valida las credenciales y crea una sesión. Devuelve el session ID.
|
|
///
|
|
/// Acepta nombre de usuario o email como primer parámetro. Aplica la política de bloqueo
|
|
/// por intentos fallidos y rehashea la contraseña si los parámetros de coste han cambiado.
|
|
pub async fn login(
|
|
username_or_email: &str,
|
|
plain_password: &str,
|
|
remember: bool,
|
|
) -> Result<String, AuthError> {
|
|
let now = Utc::now();
|
|
|
|
// Buscar usuario por username o email.
|
|
let user_model = user::Entity::find()
|
|
.filter(
|
|
Condition::any()
|
|
.add(user::Column::Username.eq(username_or_email))
|
|
.add(user::Column::Email.eq(username_or_email)),
|
|
)
|
|
.one(dbconn())
|
|
.await?
|
|
.ok_or(AuthError::InvalidCredentials)?;
|
|
|
|
// Comprobar bloqueo temporal por intentos fallidos.
|
|
if let Some(locked_until) = user_model.locked_until
|
|
&& locked_until > now
|
|
{
|
|
return Err(AuthError::AccountLocked);
|
|
}
|
|
|
|
// Comprobar estado de la cuenta.
|
|
match UserStatus::from_i16(user_model.status) {
|
|
UserStatus::Blocked => return Err(AuthError::AccountBlocked),
|
|
UserStatus::Pending => return Err(AuthError::AccountPending),
|
|
UserStatus::Active => {}
|
|
}
|
|
|
|
// Verificar contraseña.
|
|
if !password::verify_password(plain_password, &user_model.password_hash) {
|
|
register_failed_login(&user_model, now).await?;
|
|
return Err(AuthError::InvalidCredentials);
|
|
}
|
|
|
|
// Actualizar last_login_at y resetear contador de fallos.
|
|
let user_id = user_model.id;
|
|
let needs_rehash = password::needs_rehash(&user_model.password_hash);
|
|
let mut active: user::ActiveModel = user_model.into();
|
|
active.last_login_at = Set(Some(now));
|
|
active.last_access_at = Set(Some(now));
|
|
active.failed_login_count = Set(0);
|
|
active.locked_until = Set(None);
|
|
if needs_rehash {
|
|
let new_hash = password::hash_password(plain_password)?;
|
|
active.password_hash = Set(new_hash);
|
|
}
|
|
active.update(dbconn()).await?;
|
|
|
|
// Crear sesión y devolver el SID.
|
|
session::create_session(user_id, remember)
|
|
.await
|
|
.map_err(AuthError::Database)
|
|
}
|
|
|
|
// **< logout >*************************************************************************************
|
|
|
|
/// Destruye la sesión indicada.
|
|
pub async fn logout(sid: &str) -> Result<(), AuthError> {
|
|
session::destroy_session(sid)
|
|
.await
|
|
.map_err(AuthError::Database)
|
|
}
|
|
|
|
// **< register >***********************************************************************************
|
|
|
|
/// Registra un nuevo usuario. Devuelve el `user_id` asignado.
|
|
///
|
|
/// Valida: longitud de contraseña, coincidencia de confirmación, y unicidad de username y email.
|
|
pub async fn register(
|
|
username: &str,
|
|
email: &str,
|
|
plain_password: &str,
|
|
confirm_password: &str,
|
|
) -> Result<i32, AuthError> {
|
|
password::validate_strength(plain_password)?;
|
|
password::passwords_match(plain_password, confirm_password)?;
|
|
|
|
// Comprobar unicidad de username y email.
|
|
if user::Entity::find()
|
|
.filter(user::Column::Username.eq(username))
|
|
.one(dbconn())
|
|
.await?
|
|
.is_some()
|
|
{
|
|
return Err(AuthError::UsernameTaken);
|
|
}
|
|
if user::Entity::find()
|
|
.filter(user::Column::Email.eq(email))
|
|
.one(dbconn())
|
|
.await?
|
|
.is_some()
|
|
{
|
|
return Err(AuthError::EmailTaken);
|
|
}
|
|
|
|
let hash = password::hash_password(plain_password)?;
|
|
let now = Utc::now();
|
|
let status = if SETTINGS.require_email_verification {
|
|
UserStatus::Pending
|
|
} else {
|
|
UserStatus::Active
|
|
};
|
|
|
|
let new_user = user::ActiveModel {
|
|
id: ActiveValue::NotSet,
|
|
username: Set(username.to_owned()),
|
|
email: Set(email.to_owned()),
|
|
email_verified_at: Set(None),
|
|
password_hash: Set(hash),
|
|
status: Set(status.as_i16()),
|
|
language: Set(None),
|
|
timezone: Set(None),
|
|
theme: Set(None),
|
|
display_name: Set(None),
|
|
about: Set(None),
|
|
last_login_at: Set(None),
|
|
last_access_at: Set(None),
|
|
failed_login_count: Set(0),
|
|
locked_until: Set(None),
|
|
is_admin: Set(false),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
};
|
|
let result = user::Entity::insert(new_user).exec(dbconn()).await?;
|
|
let user_id = result.last_insert_id;
|
|
|
|
Ok(user_id)
|
|
}
|
|
|
|
// **< assign_role >********************************************************************************
|
|
|
|
/// Asigna un rol a un usuario (sin error si ya está asignado).
|
|
pub async fn assign_role(user_id: i32, role_id: i32) -> Result<(), AuthError> {
|
|
let already = user_role::Entity::find()
|
|
.filter(user_role::Column::UserId.eq(user_id))
|
|
.filter(user_role::Column::RoleId.eq(role_id))
|
|
.one(dbconn())
|
|
.await?;
|
|
|
|
if already.is_none() {
|
|
user_role::Entity::insert(user_role::ActiveModel {
|
|
user_id: Set(user_id),
|
|
role_id: Set(role_id),
|
|
})
|
|
.exec(dbconn())
|
|
.await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// **< change_own_password >************************************************************************
|
|
|
|
/// Cambia la contraseña del propio usuario tras comprobar la actual, y cierra el resto de sus
|
|
/// sesiones abiertas conservando la indicada en `current_sid` (la de quien hace el cambio).
|
|
///
|
|
/// Devuelve [`AuthError::InvalidCredentials`] si la contraseña actual no es correcta.
|
|
pub(crate) async fn change_own_password(
|
|
user_id: i32,
|
|
current_password: &str,
|
|
new_password: &str,
|
|
current_sid: Option<&str>,
|
|
) -> Result<(), AuthError> {
|
|
let user_model = user::Entity::find_by_id(user_id)
|
|
.one(dbconn())
|
|
.await?
|
|
.ok_or(AuthError::UserNotFound)?;
|
|
if !password::verify_password(current_password, &user_model.password_hash) {
|
|
return Err(AuthError::InvalidCredentials);
|
|
}
|
|
password::validate_strength(new_password)?;
|
|
let hash = password::hash_password(new_password)?;
|
|
|
|
user::ActiveModel {
|
|
id: Set(user_id),
|
|
password_hash: Set(hash),
|
|
updated_at: Set(Utc::now()),
|
|
..Default::default()
|
|
}
|
|
.update(dbconn())
|
|
.await?;
|
|
|
|
match current_sid {
|
|
Some(sid) => session::destroy_other_sessions(user_id, sid).await?,
|
|
None => session::destroy_user_sessions(user_id).await?,
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
// **< register_failed_login >**********************************************************************
|
|
|
|
async fn register_failed_login(
|
|
user_model: &user::Model,
|
|
now: DateTime<Utc>,
|
|
) -> Result<(), AuthError> {
|
|
let new_count = user_model.failed_login_count + 1;
|
|
let lock_at = if new_count >= SETTINGS.max_failed_logins {
|
|
Some(now + Duration::seconds(SETTINGS.locked_for_secs))
|
|
} else {
|
|
None
|
|
};
|
|
user::ActiveModel {
|
|
id: Set(user_model.id),
|
|
failed_login_count: Set(new_count),
|
|
locked_until: Set(lock_at),
|
|
updated_at: Set(now),
|
|
..Default::default()
|
|
}
|
|
.update(dbconn())
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
// **< seed_initial_data >**************************************************************************
|
|
|
|
/// Prepara una instalación nueva si no existe ningún usuario en la base de datos.
|
|
///
|
|
/// Se llama desde `Extension::initialize()`. Si la tabla está vacía, crea el administrador
|
|
/// con las credenciales configuradas en `[user.seed]`. La contraseña se genera aleatoriamente
|
|
/// si no está configurada, y se imprime por stdout una sola vez para que el operador la recoja.
|
|
///
|
|
/// Concede además al rol "authenticated" los permisos que todo usuario espera tener sobre su
|
|
/// propia cuenta: editar su perfil y cambiar su contraseña. Sólo en la instalación nueva; después
|
|
/// los gestiona el administrador desde la interfaz.
|
|
pub(crate) async fn seed_initial_data() {
|
|
do_seed().await;
|
|
}
|
|
|
|
async fn do_seed() {
|
|
let count = user::Entity::find().count(dbconn()).await.unwrap_or(1);
|
|
if count > 0 {
|
|
return;
|
|
}
|
|
|
|
let cfg = &SETTINGS.seed;
|
|
let (admin_password, generated) = match &cfg.admin_password {
|
|
Some(p) if !p.is_empty() => (p.clone(), false),
|
|
_ => {
|
|
// Generar contraseña aleatoria de 20 caracteres.
|
|
use base64ct::{Base64UrlUnpadded, Encoding};
|
|
use rand_core::{OsRng, RngCore};
|
|
let mut bytes = [0u8; 15];
|
|
OsRng.fill_bytes(&mut bytes);
|
|
(Base64UrlUnpadded::encode_string(&bytes), true)
|
|
}
|
|
};
|
|
|
|
let hash = match password::hash_password(&admin_password) {
|
|
Ok(h) => h,
|
|
Err(e) => {
|
|
eprintln!(
|
|
"pagetop-user seed error: failed to hash admin password: {}",
|
|
e
|
|
);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let now = Utc::now();
|
|
let new_admin = user::ActiveModel {
|
|
id: ActiveValue::NotSet,
|
|
username: Set(cfg.admin_username.clone()),
|
|
email: Set(cfg.admin_email.clone()),
|
|
email_verified_at: Set(Some(now)),
|
|
password_hash: Set(hash),
|
|
status: Set(UserStatus::Active.as_i16()),
|
|
language: Set(None),
|
|
timezone: Set(None),
|
|
theme: Set(None),
|
|
display_name: Set(Some("Administrator".into())),
|
|
about: Set(None),
|
|
last_login_at: Set(None),
|
|
last_access_at: Set(None),
|
|
failed_login_count: Set(0),
|
|
locked_until: Set(None),
|
|
is_admin: Set(true),
|
|
created_at: Set(now),
|
|
updated_at: Set(now),
|
|
};
|
|
|
|
match user::Entity::insert(new_admin).exec(dbconn()).await {
|
|
Ok(_) => {
|
|
if generated {
|
|
println!(
|
|
"\npagetop-user: admin account created.\n username: {}\n password: {}\n",
|
|
cfg.admin_username, admin_password
|
|
);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!("pagetop-user seed error: {}", e);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let defaults = [
|
|
UserPermission::EditOwnProfile,
|
|
UserPermission::ChangeOwnPassword,
|
|
];
|
|
let rows = defaults.map(|perm| role_permission::ActiveModel {
|
|
role_id: Set(crate::AUTHENTICATED_ROLE_ID),
|
|
permission_key: Set(perm.key().into_owned()),
|
|
granted_at: Set(now),
|
|
});
|
|
if let Err(e) = role_permission::Entity::insert_many(rows)
|
|
.exec(dbconn())
|
|
.await
|
|
{
|
|
eprintln!(
|
|
"pagetop-user seed error: failed to grant default permissions: {}",
|
|
e
|
|
);
|
|
}
|
|
}
|