This commit is contained in:
Andras Schmelczer 2026-03-20 07:52:06 +00:00
parent f32a552f46
commit 05b8ee06c1
5 changed files with 28 additions and 16 deletions

View file

@ -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<Instant> = 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()));

View file

@ -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);

View file

@ -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}",

View file

@ -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))?;
}
let quant = state.data.quant_ref();
let (parsed_filters, parsed_enum_filters) = parse_filters(

View file

@ -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 {