diff --git a/server-rs/src/auth.rs b/server-rs/src/auth.rs index b22ccb5..9ac6be5 100644 --- a/server-rs/src/auth.rs +++ b/server-rs/src/auth.rs @@ -55,9 +55,13 @@ impl TokenCache { // Evict expired entries first let now = Instant::now(); map.retain(|_, (_, created)| now.duration_since(*created).as_secs() < TOKEN_TTL_SECS); - // If still too many, clear all + // If still too many, evict oldest half instead of clearing all + // (avoids thundering herd where every request re-validates at once) if map.len() >= MAX_CACHE_ENTRIES { - map.clear(); + let mut ages: Vec = map.values().map(|(_, created)| *created).collect(); + ages.sort(); + let median = ages[ages.len() / 2]; + map.retain(|_, (_, created)| *created >= median); } } map.insert(token, (user, Instant::now())); diff --git a/server-rs/src/consts.rs b/server-rs/src/consts.rs index c5c0b1d..c2976be 100644 --- a/server-rs/src/consts.rs +++ b/server-rs/src/consts.rs @@ -28,8 +28,3 @@ pub const SERVICE_CALL_TIMEOUT: u64 = 120; /// Inner London free zone bounds (south, west, north, east) — roughly zone 1. /// Users without a license can only query data within these bounds. pub const FREE_ZONE_BOUNDS: (f64, f64, f64, f64) = (51.44, -0.31, 51.59, 0.05); - -/// Exact demo bounds (south, west, north, east) sent by the homepage ScrollStory. -/// Requests matching these exact values bypass the license check so the -/// animation works for anonymous visitors. Only this specific viewport is allowed. -pub const DEMO_BOUNDS: (f64, f64, f64, f64) = (46.0, -12.0, 56.5, 12.0); diff --git a/server-rs/src/main.rs b/server-rs/src/main.rs index 2404ac0..b0aff6b 100644 --- a/server-rs/src/main.rs +++ b/server-rs/src/main.rs @@ -435,10 +435,19 @@ async fn main() -> anyhow::Result<()> { let api = Router::new() .route("/api/features", get(routes::get_features)) - .route("/api/hexagons", get(routes::get_hexagons)) - .route("/api/postcodes", get(routes::get_postcodes)) + .route( + "/api/hexagons", + get(routes::get_hexagons).layer(ConcurrencyLimitLayer::new(20)), + ) + .route( + "/api/postcodes", + get(routes::get_postcodes).layer(ConcurrencyLimitLayer::new(20)), + ) .route("/api/postcode/{postcode}", get(routes::get_postcode_lookup)) - .route("/api/pois", get(routes::get_pois)) + .route( + "/api/pois", + get(routes::get_pois).layer(ConcurrencyLimitLayer::new(20)), + ) .route("/api/poi-categories", get(routes::get_poi_categories)) .route("/api/places", get(routes::get_places)) .route("/api/travel-modes", get(routes::get_travel_modes)) @@ -485,7 +494,10 @@ async fn main() -> anyhow::Result<()> { .route("/s/{code}", get(routes::get_short_url)) .route("/api/telemetry", post(routes::post_telemetry)) .route("/api/reload", post(routes::post_reload)) - .route("/pb/{*rest}", any(routes::proxy_to_pocketbase)) + .route( + "/pb/{*rest}", + any(routes::proxy_to_pocketbase).layer(ConcurrencyLimitLayer::new(10)), + ) // Tile routes use a different state type — kept as closures .route( "/api/tiles/{z}/{x}/{y}", diff --git a/server-rs/src/routes/hexagons.rs b/server-rs/src/routes/hexagons.rs index a91fd27..8fc570d 100644 --- a/server-rs/src/routes/hexagons.rs +++ b/server-rs/src/routes/hexagons.rs @@ -13,7 +13,7 @@ use tracing::info; use crate::aggregation::Aggregator; use crate::auth::OptionalUser; -use crate::consts::{DEMO_BOUNDS, MAX_CELLS_PER_REQUEST}; +use crate::consts::MAX_CELLS_PER_REQUEST; use crate::data::travel_time::TravelData; use crate::licensing::check_license_bounds; use crate::parsing::{ @@ -193,10 +193,7 @@ pub async fn get_hexagons( let (south, west, north, east) = require_bounds(params.bounds).map_err(IntoResponse::into_response)?; - let is_demo_view = (south, west, north, east) == DEMO_BOUNDS; - if !is_demo_view { - check_license_bounds(&user.0, (south, west, north, east))?; - } + check_license_bounds(&user.0, (south, west, north, east))?; let quant = state.data.quant_ref(); let (parsed_filters, parsed_enum_filters) = parse_filters( diff --git a/server-rs/src/routes/stripe_webhook.rs b/server-rs/src/routes/stripe_webhook.rs index 39da697..81d10c6 100644 --- a/server-rs/src/routes/stripe_webhook.rs +++ b/server-rs/src/routes/stripe_webhook.rs @@ -105,6 +105,10 @@ pub async fn post_stripe_webhook( warn!("checkout.session.completed missing client_reference_id"); return StatusCode::OK.into_response(); } + if !user_id.bytes().all(|b| b.is_ascii_alphanumeric()) || user_id.len() > 20 { + warn!(user_id, "Invalid client_reference_id format in webhook"); + return StatusCode::BAD_REQUEST.into_response(); + } // Update user subscription to "licensed" via PocketBase superuser auth let token = match get_superuser_token(&state).await {