2025-03-20 09:22:31 +01:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
use axum::{
|
2026-02-07 04:02:38 +01:00
|
|
|
extract::{State, Request, FromRequestParts},
|
|
|
|
|
http::{StatusCode, HeaderMap, header, request::Parts},
|
2025-03-20 09:22:31 +01:00
|
|
|
middleware::Next,
|
|
|
|
|
response::{Response, IntoResponse},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
use crate::common::di::AppState;
|
|
|
|
|
|
2026-02-02 23:56:40 +01:00
|
|
|
// Re-export CurrentUser from application layer for use in handlers
|
|
|
|
|
pub use crate::application::dtos::user_dto::CurrentUser;
|
2025-03-20 09:22:31 +01:00
|
|
|
|
2025-03-24 16:47:42 +01:00
|
|
|
// Estructura para usar en extractores de Axum
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct AuthUser {
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub username: String,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-07 04:02:38 +01:00
|
|
|
/// Extractor reutilizable que obtiene el user_id del usuario autenticado.
|
|
|
|
|
/// Se extrae automáticamente del `CurrentUser` insertado por el auth middleware.
|
|
|
|
|
///
|
|
|
|
|
/// Uso en handlers:
|
2026-02-08 13:40:23 +01:00
|
|
|
/// ```ignore
|
2026-02-07 04:02:38 +01:00
|
|
|
/// async fn my_handler(CurrentUserId(user_id): CurrentUserId) -> impl IntoResponse { ... }
|
|
|
|
|
/// ```
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct CurrentUserId(pub String);
|
|
|
|
|
|
|
|
|
|
// Implementar FromRequestParts para AuthUser — permite usar `auth_user: AuthUser` en handlers
|
|
|
|
|
impl<S> FromRequestParts<S> for AuthUser
|
|
|
|
|
where
|
|
|
|
|
S: Send + Sync,
|
|
|
|
|
{
|
|
|
|
|
type Rejection = AuthError;
|
|
|
|
|
|
|
|
|
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
|
|
|
|
parts
|
|
|
|
|
.extensions
|
|
|
|
|
.get::<CurrentUser>()
|
|
|
|
|
.map(|cu| AuthUser {
|
|
|
|
|
id: cu.id.clone(),
|
|
|
|
|
username: cu.username.clone(),
|
|
|
|
|
})
|
|
|
|
|
.ok_or(AuthError::UserNotFound)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Implementar FromRequestParts para CurrentUserId — extractor ligero solo para el user_id
|
|
|
|
|
impl<S> FromRequestParts<S> for CurrentUserId
|
|
|
|
|
where
|
|
|
|
|
S: Send + Sync,
|
|
|
|
|
{
|
|
|
|
|
type Rejection = AuthError;
|
|
|
|
|
|
|
|
|
|
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
|
|
|
|
parts
|
|
|
|
|
.extensions
|
|
|
|
|
.get::<CurrentUser>()
|
|
|
|
|
.map(|cu| CurrentUserId(cu.id.clone()))
|
|
|
|
|
.ok_or(AuthError::UserNotFound)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-20 09:22:31 +01:00
|
|
|
// Error para las operaciones de autenticación
|
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
|
|
|
pub enum AuthError {
|
|
|
|
|
#[error("Token no proporcionado")]
|
|
|
|
|
TokenNotProvided,
|
|
|
|
|
|
|
|
|
|
#[error("Token inválido: {0}")]
|
|
|
|
|
InvalidToken(String),
|
|
|
|
|
|
|
|
|
|
#[error("Token expirado")]
|
|
|
|
|
TokenExpired,
|
|
|
|
|
|
|
|
|
|
#[error("Usuario no encontrado")]
|
|
|
|
|
UserNotFound,
|
|
|
|
|
|
|
|
|
|
#[error("Acceso denegado: {0}")]
|
|
|
|
|
AccessDenied(String),
|
2026-02-08 13:40:23 +01:00
|
|
|
|
|
|
|
|
#[error("Servicio de autenticación no disponible")]
|
|
|
|
|
AuthServiceUnavailable,
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl IntoResponse for AuthError {
|
|
|
|
|
fn into_response(self) -> Response {
|
|
|
|
|
let (status, error_message) = match self {
|
|
|
|
|
AuthError::TokenNotProvided => (StatusCode::UNAUTHORIZED, "Token no proporcionado".to_string()),
|
|
|
|
|
AuthError::InvalidToken(msg) => (StatusCode::UNAUTHORIZED, msg),
|
|
|
|
|
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expirado".to_string()),
|
|
|
|
|
AuthError::UserNotFound => (StatusCode::UNAUTHORIZED, "Usuario no encontrado".to_string()),
|
|
|
|
|
AuthError::AccessDenied(msg) => (StatusCode::FORBIDDEN, msg),
|
2026-02-08 13:40:23 +01:00
|
|
|
AuthError::AuthServiceUnavailable => (StatusCode::INTERNAL_SERVER_ERROR, "Servicio de autenticación no disponible".to_string()),
|
2025-03-20 09:22:31 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let body = axum::Json(serde_json::json!({
|
|
|
|
|
"error": error_message
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
(status, body).into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
/// Middleware de autenticación seguro.
|
|
|
|
|
///
|
|
|
|
|
/// Valida el token JWT contra el servicio de autenticación configurado.
|
|
|
|
|
/// No acepta bypasses, tokens mock, ni parámetros de URL para saltar validación.
|
2025-03-20 09:22:31 +01:00
|
|
|
pub async fn auth_middleware(
|
2026-02-03 17:59:04 +01:00
|
|
|
State(state): State<Arc<AppState>>,
|
2025-03-20 09:22:31 +01:00
|
|
|
headers: HeaderMap,
|
|
|
|
|
mut request: Request,
|
|
|
|
|
next: Next,
|
|
|
|
|
) -> Result<Response, AuthError> {
|
2026-02-08 13:40:23 +01:00
|
|
|
// Extraer el token Bearer del header Authorization
|
|
|
|
|
let token_str = headers
|
|
|
|
|
.get(header::AUTHORIZATION)
|
|
|
|
|
.and_then(|value| value.to_str().ok())
|
|
|
|
|
.and_then(|value| value.strip_prefix("Bearer "))
|
|
|
|
|
.ok_or(AuthError::TokenNotProvided)?;
|
2025-03-31 06:20:15 +02:00
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
// Validar que el token no esté vacío
|
|
|
|
|
let token_str = token_str.trim();
|
|
|
|
|
if token_str.is_empty() {
|
|
|
|
|
return Err(AuthError::TokenNotProvided);
|
2025-03-31 06:20:15 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
tracing::debug!("Processing authentication token");
|
|
|
|
|
|
|
|
|
|
// Validar el token usando el servicio de autenticación
|
|
|
|
|
if let Some(auth_service) = state.auth_service.as_ref() {
|
|
|
|
|
let token_service = &auth_service.token_service;
|
|
|
|
|
match token_service.validate_token(token_str) {
|
|
|
|
|
Ok(claims) => {
|
|
|
|
|
tracing::debug!("Token validated successfully for user: {}", claims.username);
|
2026-02-03 17:59:04 +01:00
|
|
|
let current_user = CurrentUser {
|
2026-02-08 13:40:23 +01:00
|
|
|
id: claims.sub,
|
|
|
|
|
username: claims.username,
|
|
|
|
|
email: claims.email,
|
|
|
|
|
role: claims.role,
|
2026-02-03 17:59:04 +01:00
|
|
|
};
|
|
|
|
|
request.extensions_mut().insert(current_user);
|
|
|
|
|
return Ok(next.run(request).await);
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
2026-02-08 13:40:23 +01:00
|
|
|
tracing::warn!("Token validation failed: {}", e);
|
2026-02-03 17:59:04 +01:00
|
|
|
return Err(AuthError::InvalidToken(format!("Token inválido: {}", e)));
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
// Si no hay servicio de autenticación disponible, denegar acceso
|
|
|
|
|
tracing::error!("Auth middleware invoked but auth service is not configured");
|
|
|
|
|
Err(AuthError::AuthServiceUnavailable)
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-08 13:40:23 +01:00
|
|
|
/// Middleware para verificar que el usuario autenticado tiene rol de administrador.
|
|
|
|
|
///
|
|
|
|
|
/// Debe aplicarse DESPUÉS del auth_middleware, ya que depende de que
|
|
|
|
|
/// `CurrentUser` esté presente en las extensiones de la request.
|
2025-03-20 09:22:31 +01:00
|
|
|
pub async fn require_admin(
|
2026-02-08 13:40:23 +01:00
|
|
|
request: Request,
|
2025-03-20 09:22:31 +01:00
|
|
|
next: Next,
|
|
|
|
|
) -> Response {
|
2026-02-08 13:40:23 +01:00
|
|
|
// Obtener el CurrentUser insertado por auth_middleware
|
|
|
|
|
if let Some(current_user) = request.extensions().get::<CurrentUser>() {
|
|
|
|
|
if current_user.role == "admin" {
|
|
|
|
|
tracing::debug!("Admin access granted for user: {}", current_user.username);
|
|
|
|
|
return next.run(request).await;
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
2026-02-08 13:40:23 +01:00
|
|
|
tracing::warn!("Admin access denied for user: {} (role: {})", current_user.username, current_user.role);
|
|
|
|
|
} else {
|
|
|
|
|
tracing::warn!("Admin check failed: no authenticated user in request");
|
2025-03-20 09:22:31 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Acceso denegado
|
|
|
|
|
let error = AuthError::AccessDenied("Se requiere rol de administrador".to_string());
|
|
|
|
|
error.into_response()
|
|
|
|
|
}
|