use std::net::SocketAddr;
use std::sync::Arc;
use crate::server::common::audit;
use axum::{
Json,
extract::{ConnectInfo, State},
http::StatusCode,
response::IntoResponse,
};
use manta_shared::types::auth::{
AuthTokenRequest, AuthTokenResponse, ValidateTokenRequest,
};
use super::{ErrorResponse, ServerState, SiteHeader, SiteName};
use crate::service;
fn generic_invalid_credentials() -> (StatusCode, Json<ErrorResponse>) {
(
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "invalid credentials".to_string(),
}),
)
}
#[utoipa::path(post, path = "/auth/token", tag = "auth",
params(SiteHeader),
request_body = AuthTokenRequest,
responses(
(status = 200, description = "Token issued", body = AuthTokenResponse),
(status = 401, description = "Invalid credentials", body = ErrorResponse),
(status = 429, description = "Rate limit exceeded", body = ErrorResponse),
(status = 500, description = "Internal error", body = ErrorResponse),
)
)]
#[tracing::instrument(skip_all)]
pub async fn auth_token(
State(state): State<Arc<ServerState>>,
SiteName(site_name): SiteName,
ConnectInfo(peer): ConnectInfo<SocketAddr>,
Json(req): Json<AuthTokenRequest>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
let infra = state.infra_context(&site_name).map_err(|e| {
tracing::warn!("auth_token: site lookup failed: {}", e);
generic_invalid_credentials()
})?;
let source_ip = peer.ip().to_string();
tracing::info!(
user = %req.username,
site = %site_name,
from = %source_ip,
"auth_token: credential exchange requested"
);
match service::auth::get_api_token(&infra, &req.username, &req.password).await
{
Ok(token) => {
tracing::info!(
user = %req.username,
site = %site_name,
from = %source_ip,
"auth_token: token issued"
);
audit::send_auth_audit(
state.auditor.as_ref(),
"success",
&req.username,
&source_ip,
&site_name,
)
.await;
Ok(Json(AuthTokenResponse { token }))
}
Err(e) => {
tracing::warn!(
"auth_token: backend rejected user={} site={} from={}: {}",
req.username,
site_name,
source_ip,
e
);
audit::send_auth_audit(
state.auditor.as_ref(),
"failure",
&req.username,
&source_ip,
&site_name,
)
.await;
Err(generic_invalid_credentials())
}
}
}
#[utoipa::path(post, path = "/auth/validate", tag = "auth",
params(SiteHeader),
request_body = ValidateTokenRequest,
responses(
(status = 200, description = "Token is valid"),
(status = 401, description = "Token rejected", body = ErrorResponse),
(status = 429, description = "Rate limit exceeded", body = ErrorResponse),
(status = 500, description = "Internal error", body = ErrorResponse),
)
)]
#[tracing::instrument(skip_all)]
pub async fn auth_validate(
State(state): State<Arc<ServerState>>,
SiteName(site_name): SiteName,
Json(req): Json<ValidateTokenRequest>,
) -> Result<impl IntoResponse, (StatusCode, Json<ErrorResponse>)> {
let infra = state.infra_context(&site_name).map_err(|e| {
tracing::warn!("auth_validate: site lookup failed: {}", e);
generic_invalid_credentials()
})?;
tracing::info!(site = %site_name, "auth_validate: token check requested");
match service::auth::validate_api_token(&infra, &req.token).await {
Ok(()) => {
tracing::info!(site = %site_name, "auth_validate: token accepted");
Ok(StatusCode::OK)
}
Err(e) => {
tracing::warn!("auth_validate: backend rejected token: {}", e);
Err(generic_invalid_credentials())
}
}
}