use std::sync::Arc;
use axum::{
extract::{ConnectInfo, Path, State},
http::{header, HeaderMap, StatusCode},
response::{IntoResponse, Response},
routing::{get, post},
Json, Router,
};
use crate::application::service::capability::capability_secret_from_env;
use crate::application::service::event_error::EventError;
use crate::application::service::intake_service::{
FixedWindows, IntakePayload, IntakeService, IntakeThrottles, EVENT_TRUSTED_PROXY_ENV,
};
use crate::application::service::ics_service::IcsService;
use crate::application::service::my_tickets_service::MyTicketsService;
use crate::application::service::registration_service::RegistrationCommandService;
use crate::application::service::template_port::{EventMailQueue, EventTemplateRenderer};
use crate::infrastructure::persistence::event_command_repository::EventCommandRepository;
use crate::infrastructure::persistence::seat_repository::SeatRepository;
pub const ICS_THROTTLE: (u64, u64) = (30, 60);
#[derive(Clone)]
pub struct EventPublicState {
secret: String,
trusted_proxy: bool,
ics: Arc<IcsService>,
tickets: Arc<MyTicketsService>,
intake: Arc<IntakeService>,
windows: Arc<FixedWindows>,
}
impl EventPublicState {
pub fn compose(
pool: sqlx::PgPool,
renderer: Arc<dyn EventTemplateRenderer>,
queue: Arc<dyn EventMailQueue>,
) -> Self {
Self::with_secret_and_trusted_proxy(
pool,
renderer,
queue,
capability_secret_from_env(),
trusted_proxy_from_env(),
)
}
pub fn with_secret(
pool: sqlx::PgPool,
renderer: Arc<dyn EventTemplateRenderer>,
queue: Arc<dyn EventMailQueue>,
secret: String,
) -> Self {
Self::with_secret_and_trusted_proxy(pool, renderer, queue, secret, false)
}
pub fn with_secret_and_trusted_proxy(
pool: sqlx::PgPool,
renderer: Arc<dyn EventTemplateRenderer>,
queue: Arc<dyn EventMailQueue>,
secret: String,
trusted_proxy: bool,
) -> Self {
let events = EventCommandRepository::new(pool.clone());
let seats = SeatRepository::new(pool.clone());
let _ = (&renderer, &queue); Self {
secret,
trusted_proxy,
ics: Arc::new(IcsService::new(EventCommandRepository::new(pool.clone()))),
tickets: Arc::new(MyTicketsService::new(events, seats)),
intake: Arc::new(IntakeService::new(
RegistrationCommandService::new(SeatRepository::new(pool)),
IntakeThrottles::default(),
)),
windows: Arc::new(FixedWindows::new()),
}
}
pub fn secret_is_configured(&self) -> bool {
!self.secret.is_empty()
}
}
pub fn event_public_routes(state: EventPublicState) -> Router {
Router::new()
.route("/public/ics/:capability", get(ics_handler))
.route("/public/my-tickets/:capability", get(my_tickets_handler))
.with_state(state)
}
pub fn event_intake_routes(state: EventPublicState) -> Router {
Router::new()
.route("/intake/:event_id", post(intake_handler))
.with_state(state)
}
pub fn trusted_proxy_from_env() -> bool {
matches!(
std::env::var(EVENT_TRUSTED_PROXY_ENV)
.unwrap_or_default()
.trim()
.to_ascii_lowercase()
.as_str(),
"true" | "1" | "yes" | "on"
)
}
pub fn caller_ip(headers: &HeaderMap, socket_ip: Option<&str>, trusted_proxy: bool) -> String {
if trusted_proxy {
if let Some(fwd) = headers.get("x-forwarded-for").and_then(|v| v.to_str().ok()) {
if let Some(last) = fwd.rsplit(',').next() {
let trimmed = last.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
}
}
socket_ip.unwrap_or("unknown").to_string()
}
async fn ics_handler(
State(state): State<EventPublicState>,
Path(capability): Path<String>,
headers: HeaderMap,
connect_info: Option<ConnectInfo<std::net::SocketAddr>>,
) -> Response {
if !state.secret_is_configured() {
return EventError::EventCapabilitySecretNotConfigured.into_response();
}
let ip = caller_ip(
&headers,
connect_info.map(|c| c.ip().to_string()).as_deref(),
state.trusted_proxy,
);
let (max, window) = ICS_THROTTLE;
if !state.windows.allow(&format!("ics-token:{capability}"), max, window)
|| !state.windows.allow(&format!("ics-ip:{ip}"), max, window)
{
return EventError::EventThrottled {
retry_after_secs: window as u32,
}
.into_response();
}
match state.ics.feed(&state.secret, &capability).await {
Ok(feed) => (
StatusCode::OK,
[(header::CONTENT_TYPE, "text/calendar; charset=utf-8")],
feed.body,
)
.into_response(),
Err(e) => e.into_response(),
}
}
async fn my_tickets_handler(
State(state): State<EventPublicState>,
Path(capability): Path<String>,
) -> Response {
if !state.secret_is_configured() {
return EventError::EventCapabilitySecretNotConfigured.into_response();
}
match state.tickets.report(&state.secret, &capability).await {
Ok(report) => (StatusCode::OK, Json(report)).into_response(),
Err(e) => e.into_response(),
}
}
async fn intake_handler(
State(state): State<EventPublicState>,
Path(event_id): Path<uuid::Uuid>,
headers: HeaderMap,
connect_info: Option<ConnectInfo<std::net::SocketAddr>>,
Json(payload): Json<IntakePayload>,
) -> Response {
let ip = caller_ip(
&headers,
connect_info.map(|c| c.ip().to_string()).as_deref(),
state.trusted_proxy,
);
match state.intake.intake(event_id, payload, &ip).await {
Ok(row) => (StatusCode::CREATED, Json(row)).into_response(),
Err(e) => e.into_response(),
}
}