use axum::{
extract::State,
http::{HeaderMap, StatusCode},
response::IntoResponse,
Json,
};
use gradatum_acl_auth::ApiKeyError;
use gradatum_auth::jwt::TokenScope;
use serde::Serialize;
use crate::state::AppState;
#[derive(Debug, Serialize, serde::Deserialize)]
pub struct ExchangeResponse {
pub token: String,
pub ttl_secs: u64,
pub scopes: Vec<String>,
pub tenant_id: String,
pub kid: String,
}
#[derive(Debug, Serialize)]
struct ErrorResponse {
error: &'static str,
}
pub async fn exchange(State(state): State<AppState>, headers: HeaderMap) -> impl IntoResponse {
let secret = match extract_api_key_secret(&headers) {
Some(s) => s,
None => {
return (
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error:
"Authorization header absent ou format invalide (attendu: Bearer ak_...)",
}),
)
.into_response();
}
};
let prefix_display = if secret.len() >= 11 {
&secret[..11] } else {
&secret[..]
};
tracing::debug!(prefix = %prefix_display, "tentative d'échange API key → JWT");
let key = match state.api_keys.verify(&secret).await {
Ok(k) => k,
Err(ApiKeyError::AlreadyRevoked) => {
tracing::warn!(prefix = %prefix_display, "tentative d'échange avec clé révoquée");
return (
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "clé API invalide ou révoquée",
}),
)
.into_response();
}
Err(ApiKeyError::NotFound) => {
tracing::debug!(prefix = %prefix_display, "clé API non trouvée ou secret incorrect");
return (
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "clé API invalide ou révoquée",
}),
)
.into_response();
}
Err(e) => {
tracing::error!(error = %e, "erreur interne lors de la vérification API key");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "erreur interne — réessayer plus tard",
}),
)
.into_response();
}
};
let ttl_secs = state.jwt.ttl_service_secs();
let token = match state
.jwt
.sign(&key.owner, &key.scopes, TokenScope::Service, &key.tenant_id)
{
Ok(t) => t,
Err(e) => {
tracing::error!(error = %e, "erreur de signature JWT lors de l'échange");
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "erreur interne — réessayer plus tard",
}),
)
.into_response();
}
};
tracing::info!(
owner = %key.owner,
tenant = %key.tenant_id,
prefix = %prefix_display,
"échange API key → JWT réussi"
);
(
StatusCode::OK,
Json(ExchangeResponse {
token,
ttl_secs,
scopes: key.scopes.clone(),
tenant_id: key.tenant_id.clone(),
kid: state.jwt.kid().to_string(),
}),
)
.into_response()
}
fn extract_api_key_secret(headers: &HeaderMap) -> Option<String> {
let auth = headers.get("Authorization")?.to_str().ok()?;
if let Some(rest) = auth.strip_prefix("Bearer ") {
let trimmed = rest.trim();
if trimmed.starts_with("ak_") {
return Some(trimmed.to_string());
}
}
let trimmed = auth.trim();
if trimmed.starts_with("ak_") {
return Some(trimmed.to_string());
}
None
}
pub fn router() -> axum::Router<AppState> {
use axum::{routing::post, Router};
Router::new().route("/auth/exchange", post(exchange))
}