Update and syc dev branch #3

Merged
KYRBOT merged 44 commits from main into dev 2026-04-05 18:55:50 +00:00
140 changed files with 18534 additions and 4131 deletions
Showing only changes of commit e629004801 - Show all commits

View File

@@ -14,15 +14,32 @@ pub struct Claims {
pub exp: usize, pub exp: usize,
} }
pub struct AuthError(pub anyhow::Error); #[derive(Debug)]
pub enum AuthError {
InvalidCredentials,
EmailAlreadyExists,
AccountDisabled,
InvalidInput(String),
Internal(anyhow::Error),
}
impl IntoResponse for AuthError { impl IntoResponse for AuthError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
tracing::error!("Auth error: {:?}", self.0); let (status, error_message) = match self {
AuthError::InvalidCredentials => (StatusCode::UNAUTHORIZED, "Credenciales inválidas".to_string()),
AuthError::EmailAlreadyExists => (StatusCode::CONFLICT, "El correo ya se encuentra registrado".to_string()),
AuthError::AccountDisabled => (StatusCode::FORBIDDEN, "Tu cuenta ha sido desactivada. Contacta al administrador.".to_string()),
AuthError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, msg),
AuthError::Internal(ref e) => {
tracing::error!("Auth internal error: {:?}", e);
(StatusCode::INTERNAL_SERVER_ERROR, "Error interno del servidor".to_string())
}
};
( (
StatusCode::UNAUTHORIZED, status,
Json(serde_json::json!({ Json(serde_json::json!({
"error": "Unauthorized" "error": error_message
})), })),
) )
.into_response() .into_response()

View File

@@ -29,6 +29,7 @@ struct UserRow {
id: uuid::Uuid, id: uuid::Uuid,
password_hash: String, password_hash: String,
role_name: String, role_name: String,
is_active: bool,
} }
pub async fn login( pub async fn login(
@@ -37,7 +38,7 @@ pub async fn login(
) -> Result<impl IntoResponse, AuthError> { ) -> Result<impl IntoResponse, AuthError> {
let user = sqlx::query_as::<sqlx::Postgres, UserRow>( let user = sqlx::query_as::<sqlx::Postgres, UserRow>(
r#" r#"
SELECT u.id, u.password_hash, r.name as role_name SELECT u.id, u.password_hash, r.name as role_name, u.is_active
FROM users u FROM users u
JOIN roles r ON u.role_id = r.id JOIN roles r ON u.role_id = r.id
WHERE u.email = $1 WHERE u.email = $1
@@ -48,19 +49,34 @@ pub async fn login(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("Database error: {}", e); tracing::error!("Database error: {}", e);
AuthError(anyhow::anyhow!("Internal Server Error")) AuthError::Internal(anyhow::anyhow!("Error de base de datos"))
})?; })?;
if let Some(user) = user { if let Some(user) = user {
// 1. Verificar si la cuenta está activa
if !user.is_active {
return Err(AuthError::AccountDisabled);
}
let parsed_hash = PasswordHash::new(&user.password_hash) let parsed_hash = PasswordHash::new(&user.password_hash)
.map_err(|_| AuthError(anyhow::anyhow!("Invalid hash")))?; .map_err(|_| AuthError::Internal(anyhow::anyhow!("Hash inválido")))?;
if Argon2::default() if Argon2::default()
.verify_password(payload.password.as_bytes(), &parsed_hash) .verify_password(payload.password.as_bytes(), &parsed_hash)
.is_ok() .is_ok()
{ {
// 2. Actualizar último inicio de sesión (asíncrono)
let pool_clone = pool.clone();
let user_id = user.id;
tokio::spawn(async move {
let _ = sqlx::query("UPDATE users SET last_login = NOW() WHERE id = $1")
.bind(user_id)
.execute(&pool_clone)
.await;
});
let token = create_jwt(&user.id.to_string(), &user.role_name) let token = create_jwt(&user.id.to_string(), &user.role_name)
.map_err(|_| AuthError(anyhow::anyhow!("Token creation failed")))?; .map_err(|_| AuthError::Internal(anyhow::anyhow!("Error al crear token")))?;
return Ok(Json(json!({ return Ok(Json(json!({
"token": token, "token": token,
@@ -69,7 +85,7 @@ pub async fn login(
} }
} }
Err(AuthError(anyhow::anyhow!("Invalid credentials"))) Err(AuthError::InvalidCredentials)
} }
#[derive(sqlx::FromRow)] #[derive(sqlx::FromRow)]
@@ -81,14 +97,27 @@ pub async fn register(
State(pool): State<DbPool>, State(pool): State<DbPool>,
Json(payload): Json<RegisterPayload>, Json(payload): Json<RegisterPayload>,
) -> Result<impl IntoResponse, AuthError> { ) -> Result<impl IntoResponse, AuthError> {
// 1. Hash the password // 1. Validaciones de entrada
if payload.email.is_empty() || !payload.email.contains('@') {
return Err(AuthError::InvalidInput("Formato de correo electrónico inválido".to_string()));
}
if payload.password.len() < 8 {
return Err(AuthError::InvalidInput("La contraseña debe tener al menos 8 caracteres".to_string()));
}
if payload.full_name.trim().is_empty() {
return Err(AuthError::InvalidInput("El nombre completo es obligatorio".to_string()));
}
// 2. Cifrar la contraseña
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default(); let argon2 = Argon2::default();
let password_hash = argon2 let password_hash = argon2
.hash_password(payload.password.as_bytes(), &salt) .hash_password(payload.password.as_bytes(), &salt)
.map_err(|e| { .map_err(|e| {
tracing::error!("Hashing error: {}", e); tracing::error!("Hashing error: {}", e);
AuthError(anyhow::anyhow!("Internal Server Error")) AuthError::Internal(anyhow::anyhow!("Error de cifrado"))
})? })?
.to_string(); .to_string();
@@ -108,10 +137,11 @@ pub async fn register(
.await .await
.map_err(|e| { .map_err(|e| {
tracing::error!("User creation error: {}", e); tracing::error!("User creation error: {}", e);
if e.to_string().contains("unique constraint") { let err_str = e.to_string().to_lowercase();
AuthError(anyhow::anyhow!("Email already registered")) if err_str.contains("unique constraint") || err_str.contains("already exists") {
AuthError::EmailAlreadyExists
} else { } else {
AuthError(anyhow::anyhow!("Internal Server Error")) AuthError::Internal(anyhow::anyhow!("Error interno al crear usuario"))
} }
})?; })?;