use std::sync::Arc;
use axum::{Json, Router, extract::State, http::StatusCode, response::IntoResponse, routing::post};
use serde::Deserialize;
use super::tracking::{PgSendTracker, SendCorrelator, SendTracker, SuppressionReason};
pub struct SuppressionAdminState {
tracker: Arc<PgSendTracker>,
address_hash_key: Arc<[u8]>,
}
impl SuppressionAdminState {
#[must_use]
pub const fn new(tracker: Arc<PgSendTracker>, address_hash_key: Arc<[u8]>) -> Self {
Self {
tracker,
address_hash_key,
}
}
}
pub fn suppression_admin_router(state: Arc<SuppressionAdminState>) -> Router {
Router::new()
.route("/api/email/suppress", post(suppress))
.route("/api/email/suppression", post(query))
.with_state(state)
}
#[derive(Debug, Deserialize)]
struct SuppressRequest {
address: String,
reason: String,
#[serde(default)]
tenant: Option<String>,
}
#[derive(Debug, Deserialize)]
struct SuppressionQuery {
address: String,
#[serde(default)]
tenant: Option<String>,
}
async fn suppress(
State(state): State<Arc<SuppressionAdminState>>,
Json(request): Json<SuppressRequest>,
) -> impl IntoResponse {
let Some(reason) = SuppressionReason::parse(&request.reason) else {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({
"error": format!("unknown suppression reason {:?}", request.reason)
})),
);
};
let hash = fraiseql_observers::hash_address(&state.address_hash_key, &request.address);
let ttl = reason.default_ttl(chrono::Utc::now());
match state.tracker.suppress(request.tenant.as_deref(), &hash, reason, ttl).await {
Ok(()) => (
StatusCode::OK,
Json(serde_json::json!({ "suppressed": true, "reason": reason.as_str() })),
),
Err(error) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": error.to_string() })),
),
}
}
async fn query(
State(state): State<Arc<SuppressionAdminState>>,
Json(request): Json<SuppressionQuery>,
) -> impl IntoResponse {
let hash = fraiseql_observers::hash_address(&state.address_hash_key, &request.address);
match state.tracker.suppression_reason(request.tenant.as_deref(), &hash).await {
Ok(reason) => (
StatusCode::OK,
Json(serde_json::json!({
"suppressed": reason.is_some(),
"reason": reason.map(SuppressionReason::as_str),
})),
),
Err(error) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({ "error": error.to_string() })),
),
}
}
#[cfg(test)]
mod tests;