bgustscraper 0.2.0

Advanced semantic scraping engine with AI-driven compliance checks and legal terms validation
Documentation
use axum::{
    routing::{get, post},
    Router,
    Json,
    http::{StatusCode, request::Parts},
    extract::FromRequestParts,
    response::IntoResponse,
    async_trait,
};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::fs;
use std::net::SocketAddr;
use tower_http::cors::CorsLayer;
use crate::auth::{Claims, generate_jwt, validate_jwt, verify_credentials};
use crate::cache::LocalCache;

#[derive(Deserialize)]
struct AuthPayload {
    username: String,
    password: String,
}

#[derive(Serialize)]
struct AuthResponse {
    token: String,
}

#[derive(Serialize)]
pub struct ErrorResponse {
    pub error: String,
}

// Extractor idiomático en Axum para validar el Bearer Token JWT
#[async_trait]
impl<S> FromRequestParts<S> for Claims
where
    S: Send + Sync,
{
    type Rejection = (StatusCode, Json<ErrorResponse>);

    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
        let auth_header = parts
            .headers
            .get("Authorization")
            .and_then(|value| value.to_str().ok())
            .ok_or_else(|| {
                (
                    StatusCode::UNAUTHORIZED,
                    Json(ErrorResponse {
                        error: "Cabecera Authorization ausente".to_string(),
                    }),
                )
            })?;

        if !auth_header.starts_with("Bearer ") {
            return Err((
                StatusCode::UNAUTHORIZED,
                Json(ErrorResponse {
                    error: "Formato de autenticación inválido (Debe ser Bearer <token>)".to_string(),
                }),
            ));
        }

        let token = &auth_header[7..];
        validate_jwt(token).map_err(|e| {
            (
                StatusCode::UNAUTHORIZED,
                Json(ErrorResponse {
                    error: format!("Token inválido o expirado: {}", e),
                }),
            )
        })
    }
}

// Handler POST /api/auth
async fn login_handler(Json(payload): Json<AuthPayload>) -> impl IntoResponse {
    if verify_credentials(&payload.username, &payload.password) {
        match generate_jwt(&payload.username) {
            Ok(token) => (StatusCode::OK, Json(serde_json::json!(AuthResponse { token }))).into_response(),
            Err(e) => (
                StatusCode::INTERNAL_SERVER_ERROR,
                Json(ErrorResponse {
                    error: format!("Error al generar el token: {}", e),
                }),
            ).into_response(),
        }
    } else {
        (
            StatusCode::UNAUTHORIZED,
            Json(ErrorResponse {
                error: "Usuario o contraseña incorrectos".to_string(),
            }),
        ).into_response()
    }
}

// Handler GET /api/bcv (Protegido por JWT)
async fn get_bcv_handler(_claims: Claims) -> impl IntoResponse {
    let file_path = "downloads/bcv_rates.json";
    if let Ok(content) = fs::read_to_string(file_path) {
        if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(&content) {
            return (StatusCode::OK, Json(json_val)).into_response();
        }
    }
    
    (
        StatusCode::NOT_FOUND,
        Json(ErrorResponse {
            error: "No se han capturado datos del BCV todavía".to_string(),
        }),
    ).into_response()
}

// Handler GET /api/cache (Protegido por JWT)
async fn get_cache_handler(_claims: Claims) -> impl IntoResponse {
    match LocalCache::new() {
        Ok(_cache) => {
            // Dado que redb no expone una API simple para listar todas las claves fácilmente 
            // sin iterar sobre los buckets de bajo nivel, retornamos metadatos de estado general
            // y la información de si la caché está lista.
            (StatusCode::OK, Json(serde_json::json!({
                "status": "active",
                "storage_engine": "redb",
                "database_path": "downloads/cache.db",
                "info": "La base de datos embebida redb está en ejecución y securizada."
            }))).into_response()
        }
        Err(e) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(ErrorResponse {
                error: format!("Error de conexión con la caché redb: {}", e),
            }),
        ).into_response()
    }
}

// Handler GET /api/openapi.json (Público)
async fn get_openapi_handler() -> impl IntoResponse {
    let file_path = "config/openapi.json";
    if let Ok(content) = fs::read_to_string(file_path) {
        if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(&content) {
            return (StatusCode::OK, Json(json_val)).into_response();
        }
    }
    
    (
        StatusCode::NOT_FOUND,
        Json(serde_json::json!({ "error": "Especificación OpenAPI no encontrada" })),
    ).into_response()
}

pub async fn start_api_server(port: u16) -> Result<()> {
    let app = Router::new()
        .route("/api/auth", post(login_handler))
        .route("/api/bcv", get(get_bcv_handler))
        .route("/api/cache", get(get_cache_handler))
        .route("/api/openapi.json", get(get_openapi_handler))
        .layer(CorsLayer::permissive());

    let addr = SocketAddr::from(([0, 0, 0, 0], port));
    println!("🚀 Servidor Web API escuchando de forma segura en: http://{}", addr);
    
    let listener = tokio::net::TcpListener::bind(addr).await?;
    axum::serve(listener, app).await?;
    
    Ok(())
}