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,
}
#[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),
}),
)
})
}
}
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()
}
}
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()
}
async fn get_cache_handler(_claims: Claims) -> impl IntoResponse {
match LocalCache::new() {
Ok(_cache) => {
(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()
}
}
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(())
}