use std::sync::Arc;
use axum::extract::{Form, Query, State};
use axum::http::StatusCode;
use axum::response::{Html, IntoResponse, Response};
use axum::routing::{get, post};
use axum::Router;
use serde::Deserialize;
use crate::application::service::unsubscribe_service::UnsubscribeService;
use crate::DigestModule;
pub type ApiState = Arc<DigestModule>;
#[derive(Debug, Deserialize, Default)]
pub struct UnsubscribeTokenParam {
pub t: Option<String>,
}
pub fn public_composer() -> Router<ApiState> {
use axum::middleware as axum_mw;
Router::<ApiState>::new()
.route("/digest/unsubscribe", post(unsubscribe_one_click).get(unsubscribe_human))
.route_layer(axum_mw::from_fn_with_state(
backbone_rate_limit::middleware(120, 60),
backbone_rate_limit::rate_limit_middleware,
))
}
async fn unsubscribe_one_click(
State(module): State<ApiState>,
Query(params): Query<UnsubscribeTokenParam>,
Form(form): Form<UnsubscribeTokenParam>,
) -> Response {
let Some(unsub) = module.unsubscribe_service() else {
tracing::error!(
"unsubscribe POST with no token secret configured; no action taken"
);
return StatusCode::OK.into_response();
};
let token = form.t.or(params.t).unwrap_or_default();
if token.is_empty() {
return StatusCode::OK.into_response();
}
match unsub.unsubscribe_by_token(&token, chrono::Utc::now().timestamp()).await {
Ok(_) => StatusCode::OK.into_response(),
Err(e) => {
tracing::error!(error = %e, "unsubscribe apply failed; still the bare 200");
StatusCode::OK.into_response()
}
}
}
async fn unsubscribe_human(
State(module): State<ApiState>,
Query(params): Query<UnsubscribeTokenParam>,
) -> Response {
let Some(unsub) = module.unsubscribe_service() else {
return Html(
"<html><body><h2>Unsubscribe unavailable</h2>\
<p>This digest deployment has no unsubscribe signing key configured.</p></body></html>",
)
.into_response();
};
let Some(token) = params.t.filter(|t| !t.is_empty()) else {
return refused_page().into_response();
};
match unsub.unsubscribe_by_token(&token, chrono::Utc::now().timestamp()).await {
Ok(_) => Html(
"<html><body><h2>Unsubscribed</h2>\
<p>You will no longer receive this digest.</p></body></html>",
)
.into_response(),
Err(e) => {
tracing::error!(error = %e, "unsubscribe apply failed on the human leg");
refused_page().into_response()
}
}
}
fn refused_page() -> Html<&'static str> {
Html(
"<html><body><h2>Link invalid or expired</h2>\
<p>This unsubscribe link is not valid (it may have expired — links \
live 180 days and regenerate with every digest mail).</p></body></html>",
)
}