use awa_model::callback_contract::{
self, CompletePayload, FailPayload, HeartbeatPayload, SIGNATURE_HEADER,
};
use axum::extract::{Path, State};
use axum::http::HeaderMap;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use crate::error::ApiError;
use crate::state::AppState;
fn verify_signature(
headers: &HeaderMap,
callback_id: &str,
state: &AppState,
) -> Result<(), ApiError> {
let Some(secret) = state.callback_hmac_secret else {
return Ok(());
};
let provided = headers
.get(SIGNATURE_HEADER)
.ok_or_else(|| ApiError::unauthorized(format!("missing {SIGNATURE_HEADER} header")))?
.to_str()
.map_err(|_| ApiError::unauthorized(format!("invalid {SIGNATURE_HEADER} header")))?;
if callback_contract::verify(&secret, callback_id, provided) {
Ok(())
} else {
Err(ApiError::unauthorized("invalid callback signature"))
}
}
fn parse_timeout(timeout_seconds: f64) -> Result<std::time::Duration, ApiError> {
if !timeout_seconds.is_finite() || timeout_seconds.is_sign_negative() {
return Err(ApiError::Awa(awa_model::AwaError::Validation(
"timeout_seconds must be a finite, non-negative number".into(),
)));
}
Ok(std::time::Duration::from_secs_f64(timeout_seconds))
}
pub async fn complete_callback(
State(state): State<AppState>,
headers: HeaderMap,
Path(callback_id): Path<String>,
Json(body): Json<CompletePayload>,
) -> Result<impl IntoResponse, ApiError> {
state.require_writable()?;
verify_signature(&headers, &callback_id, &state)?;
let uuid = uuid::Uuid::parse_str(&callback_id)
.map_err(|e| ApiError::Awa(awa_model::AwaError::Validation(e.to_string())))?;
let job = awa_model::admin::complete_external(&state.pool, uuid, body.payload, None).await?;
state.invalidate_dashboard_caches();
Ok((
StatusCode::OK,
Json(serde_json::json!({
"id": job.id,
"state": format!("{:?}", job.state),
})),
))
}
pub async fn fail_callback(
State(state): State<AppState>,
headers: HeaderMap,
Path(callback_id): Path<String>,
Json(body): Json<FailPayload>,
) -> Result<impl IntoResponse, ApiError> {
state.require_writable()?;
verify_signature(&headers, &callback_id, &state)?;
let uuid = uuid::Uuid::parse_str(&callback_id)
.map_err(|e| ApiError::Awa(awa_model::AwaError::Validation(e.to_string())))?;
let job = awa_model::admin::fail_external(&state.pool, uuid, &body.error, None).await?;
state.invalidate_dashboard_caches();
Ok((
StatusCode::OK,
Json(serde_json::json!({
"id": job.id,
"state": format!("{:?}", job.state),
})),
))
}
pub async fn heartbeat_callback(
State(state): State<AppState>,
headers: HeaderMap,
Path(callback_id): Path<String>,
Json(body): Json<HeartbeatPayload>,
) -> Result<impl IntoResponse, ApiError> {
state.require_writable()?;
verify_signature(&headers, &callback_id, &state)?;
let uuid = uuid::Uuid::parse_str(&callback_id)
.map_err(|e| ApiError::Awa(awa_model::AwaError::Validation(e.to_string())))?;
let timeout = parse_timeout(body.timeout_seconds)?;
let job = awa_model::admin::heartbeat_callback(&state.pool, uuid, timeout).await?;
Ok((
StatusCode::OK,
Json(serde_json::json!({
"id": job.id,
"state": format!("{:?}", job.state),
})),
))
}