Update and syc dev branch #3
@@ -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()
|
||||||
|
|||||||
@@ -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"))
|
||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user