Files
doctate/server/src/routes/events.rs
T
Brummel 2c6062a53e refactor: drop /web/ URL prefix from browser routes
The /web/ prefix predated the /api/ split; today it just clutters every URL
without disambiguating anything. All 16 browser routes move to the apex
(/cases, /login, /magic, /events, /audio/...). The 6 /api/* routes are
unchanged. A new /->>/cases redirect closes the apex 404.

The open-redirect guard in magic.rs and case_actions.rs flips from a
positive whitelist (starts_with("/web/")) to a deny-list: same-origin path,
not protocol-relative, not under /api/, no \. The /api/ exclusion is now
load-bearing and covered by tests.

Pre-production: no transition redirects.
2026-05-04 18:36:10 +02:00

56 lines
2.0 KiB
Rust

//! SSE endpoint streaming case events to the web UI.
//!
//! One long-lived connection per browser tab. Non-admins only see events
//! for their own user_slug; admins see events across all users
//! (useful for support/monitoring). A 15s keep-alive comment keeps idle
//! connections alive through reverse proxies / NAT.
use std::convert::Infallible;
use std::time::Duration;
use axum::extract::State;
use axum::response::sse::{Event, KeepAlive, Sse};
use futures_util::stream::Stream;
use tokio_stream::StreamExt;
use tokio_stream::wrappers::BroadcastStream;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
use crate::auth::AuthenticatedWebUser;
use crate::events::EventSender;
/// `GET /events` — `text/event-stream`, one connection per tab.
pub async fn handle_events(
user: AuthenticatedWebUser,
State(events_tx): State<EventSender>,
) -> Sse<impl Stream<Item = Result<Event, Infallible>>> {
let rx = events_tx.subscribe();
let is_admin = user.is_admin();
let slug = user.slug.clone();
let stream = BroadcastStream::new(rx).filter_map(move |res| match res {
Ok(evt) => {
// Non-admins only see events for their own user_slug.
if !is_admin && evt.user_slug != slug {
return None;
}
// Serialization of `CaseEvent` is infallible in practice (plain
// struct with String + enum fields); on the unexpected error
// path we drop the event rather than tear down the stream.
let json = serde_json::to_string(&evt).ok()?;
Some(Ok(Event::default().event("case").data(json)))
}
// Lagged subscribers: skip silently. The next real event or the
// user's next navigation will resync state from the filesystem.
Err(BroadcastStreamRecvError::Lagged(n)) => {
tracing::debug!(skipped = n, "sse subscriber lagged");
None
}
});
Sse::new(stream).keep_alive(
KeepAlive::new()
.interval(Duration::from_secs(15))
.text("keepalive"),
)
}