use awa_model::callback_contract::DEFAULT_CALLBACK_PATH_PREFIX;
use axum::routing::post;
use axum::Router;
use sqlx::PgPool;
use crate::handlers;
use crate::state::AppState;
#[derive(Debug, Clone)]
pub enum CallbackAuth {
Signed([u8; 32]),
Unsigned,
}
#[derive(Debug, Clone)]
pub struct CallbackReceiverConfig {
pub auth: CallbackAuth,
pub path_prefix: String,
}
impl CallbackReceiverConfig {
pub fn new(auth: CallbackAuth) -> Self {
Self {
auth,
path_prefix: DEFAULT_CALLBACK_PATH_PREFIX.to_string(),
}
}
pub fn with_path_prefix(mut self, prefix: impl Into<String>) -> Self {
self.path_prefix = prefix.into();
self
}
}
pub async fn callback_router(
pool: PgPool,
config: CallbackReceiverConfig,
) -> Result<Router, CallbackRouterBuildError> {
let read_only = crate::state::detect_read_only(&pool).await?;
if read_only {
return Err(CallbackRouterBuildError::ReadOnlyDatabase);
}
let secret = match config.auth {
CallbackAuth::Signed(secret) => Some(secret),
CallbackAuth::Unsigned => None,
};
let state = AppState::new(pool, false, std::time::Duration::ZERO, secret);
let prefix = normalize_prefix(&config.path_prefix);
let routes = Router::new()
.route(
&format!("{prefix}/{{callback_id}}/complete"),
post(handlers::callbacks::complete_callback),
)
.route(
&format!("{prefix}/{{callback_id}}/fail"),
post(handlers::callbacks::fail_callback),
)
.route(
&format!("{prefix}/{{callback_id}}/heartbeat"),
post(handlers::callbacks::heartbeat_callback),
)
.with_state(state);
Ok(routes)
}
fn normalize_prefix(prefix: &str) -> String {
let trimmed = prefix.trim().trim_end_matches('/');
if trimmed.is_empty() {
String::new()
} else if trimmed.starts_with('/') {
trimmed.to_string()
} else {
format!("/{trimmed}")
}
}
#[derive(Debug, thiserror::Error)]
pub enum CallbackRouterBuildError {
#[error(
"callback receiver requires a writable database (pool reports transaction_read_only=on)"
)]
ReadOnlyDatabase,
#[error("failed to probe database read-only state: {0}")]
Probe(#[from] sqlx::Error),
}