Update and syc dev branch #3
@@ -14,15 +14,32 @@ pub struct Claims {
|
||||
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 {
|
||||
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!({
|
||||
"error": "Unauthorized"
|
||||
"error": error_message
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
|
||||
@@ -29,6 +29,7 @@ struct UserRow {
|
||||
id: uuid::Uuid,
|
||||
password_hash: String,
|
||||
role_name: String,
|
||||
is_active: bool,
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
@@ -37,7 +38,7 @@ pub async fn login(
|
||||
) -> Result<impl IntoResponse, AuthError> {
|
||||
let user = sqlx::query_as::<sqlx::Postgres, UserRow>(
|
||||
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
|
||||
JOIN roles r ON u.role_id = r.id
|
||||
WHERE u.email = $1
|
||||
@@ -48,19 +49,34 @@ pub async fn login(
|
||||
.await
|
||||
.map_err(|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 {
|
||||
// 1. Verificar si la cuenta está activa
|
||||
if !user.is_active {
|
||||
return Err(AuthError::AccountDisabled);
|
||||
}
|
||||
|
||||
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()
|
||||
.verify_password(payload.password.as_bytes(), &parsed_hash)
|
||||
.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)
|
||||
.map_err(|_| AuthError(anyhow::anyhow!("Token creation failed")))?;
|
||||
.map_err(|_| AuthError::Internal(anyhow::anyhow!("Error al crear token")))?;
|
||||
|
||||
return Ok(Json(json!({
|
||||
"token": token,
|
||||
@@ -69,7 +85,7 @@ pub async fn login(
|
||||
}
|
||||
}
|
||||
|
||||
Err(AuthError(anyhow::anyhow!("Invalid credentials")))
|
||||
Err(AuthError::InvalidCredentials)
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
@@ -81,14 +97,27 @@ pub async fn register(
|
||||
State(pool): State<DbPool>,
|
||||
Json(payload): Json<RegisterPayload>,
|
||||
) -> 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 argon2 = Argon2::default();
|
||||
let password_hash = argon2
|
||||
.hash_password(payload.password.as_bytes(), &salt)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Hashing error: {}", e);
|
||||
AuthError(anyhow::anyhow!("Internal Server Error"))
|
||||
AuthError::Internal(anyhow::anyhow!("Error de cifrado"))
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
@@ -108,10 +137,11 @@ pub async fn register(
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("User creation error: {}", e);
|
||||
if e.to_string().contains("unique constraint") {
|
||||
AuthError(anyhow::anyhow!("Email already registered"))
|
||||
let err_str = e.to_string().to_lowercase();
|
||||
if err_str.contains("unique constraint") || err_str.contains("already exists") {
|
||||
AuthError::EmailAlreadyExists
|
||||
} else {
|
||||
AuthError(anyhow::anyhow!("Internal Server Error"))
|
||||
AuthError::Internal(anyhow::anyhow!("Error interno al crear usuario"))
|
||||
}
|
||||
})?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user