lmao
This commit is contained in:
parent
03445188ea
commit
524580eb25
102 changed files with 36625 additions and 1295 deletions
|
|
@ -3,6 +3,7 @@ mod auth;
|
|||
mod consts;
|
||||
mod data;
|
||||
mod features;
|
||||
mod licensing;
|
||||
mod metrics;
|
||||
mod og_middleware;
|
||||
pub mod parsing;
|
||||
|
|
@ -13,15 +14,17 @@ pub mod utils;
|
|||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use axum::middleware;
|
||||
use axum::routing::{any, get, post};
|
||||
use axum::routing::{any, get, patch, post};
|
||||
use axum::Router;
|
||||
use clap::Parser;
|
||||
use tower::limit::ConcurrencyLimitLayer;
|
||||
use tower_http::compression::CompressionLayer;
|
||||
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tower_http::cors::{AllowHeaders, AllowMethods, CorsLayer};
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
use tower_http::trace::TraceLayer;
|
||||
use tracing::info;
|
||||
|
|
@ -52,7 +55,7 @@ struct Cli {
|
|||
#[arg(long)]
|
||||
tiles: PathBuf,
|
||||
|
||||
/// Path to the frontend dist directory
|
||||
/// Path to the frontend dist directory (optional; disables static serving and OG injection when omitted)
|
||||
#[arg(long)]
|
||||
dist: Option<PathBuf>,
|
||||
|
||||
|
|
@ -70,11 +73,11 @@ struct Cli {
|
|||
|
||||
/// PocketBase superuser email (for auto-creating collections at startup)
|
||||
#[arg(long, env = "POCKETBASE_ADMIN_EMAIL")]
|
||||
pocketbase_admin_email: Option<String>,
|
||||
pocketbase_admin_email: String,
|
||||
|
||||
/// PocketBase superuser password (for auto-creating collections at startup)
|
||||
#[arg(long, env = "POCKETBASE_ADMIN_PASSWORD")]
|
||||
pocketbase_admin_password: Option<String>,
|
||||
pocketbase_admin_password: String,
|
||||
|
||||
/// Ollama server URL for AI area summaries (e.g. http://ollama:11434)
|
||||
#[arg(long, env = "OLLAMA_URL")]
|
||||
|
|
@ -84,13 +87,41 @@ struct Cli {
|
|||
#[arg(long, env = "OLLAMA_MODEL")]
|
||||
ollama_model: String,
|
||||
|
||||
/// R5 routing service URL for all travel times (e.g. http://r5:8003)
|
||||
#[arg(long, env = "R5_URL")]
|
||||
r5_url: Option<String>,
|
||||
/// Path to precomputed travel times directory (contains mode subdirs with parquet files)
|
||||
#[arg(long, env = "TRAVEL_TIMES")]
|
||||
travel_times: PathBuf,
|
||||
|
||||
/// Google Maps API key for Street View metadata lookups
|
||||
#[arg(long, env = "GOOGLE_MAPS_API_KEY")]
|
||||
google_maps_api_key: String,
|
||||
|
||||
/// Stripe secret key for checkout sessions
|
||||
#[arg(long, env = "STRIPE_SECRET_KEY")]
|
||||
stripe_secret_key: String,
|
||||
|
||||
/// Stripe webhook signing secret for verifying webhook signatures
|
||||
#[arg(long, env = "STRIPE_WEBHOOK_SECRET")]
|
||||
stripe_webhook_secret: String,
|
||||
|
||||
/// Stripe Coupon ID applied when a referral code is used
|
||||
#[arg(long, env = "STRIPE_REFERRAL_COUPON_ID")]
|
||||
stripe_referral_coupon_id: String,
|
||||
|
||||
/// Google OAuth client ID for PocketBase SSO
|
||||
#[arg(long, env = "GOOGLE_OAUTH_CLIENT_ID")]
|
||||
google_oauth_client_id: String,
|
||||
|
||||
/// Google OAuth client secret for PocketBase SSO
|
||||
#[arg(long, env = "GOOGLE_OAUTH_CLIENT_SECRET")]
|
||||
google_oauth_client_secret: String,
|
||||
|
||||
/// Apple OAuth client ID for PocketBase SSO
|
||||
#[arg(long, env = "APPLE_OAUTH_CLIENT_ID")]
|
||||
apple_oauth_client_id: String,
|
||||
|
||||
/// Apple OAuth client secret for PocketBase SSO
|
||||
#[arg(long, env = "APPLE_OAUTH_CLIENT_SECRET")]
|
||||
apple_oauth_client_secret: String,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
|
|
@ -212,19 +243,23 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
let poi_category_groups = poi_data.category_groups()?;
|
||||
|
||||
// Read index.html at startup for crawler OG injection
|
||||
let (frontend_dist, index_html) = if let Some(dist) = cli.dist {
|
||||
// Read index.html at startup for crawler OG injection (only when --dist is provided)
|
||||
let index_html = if let Some(ref dist) = cli.dist {
|
||||
let index_path = dist.join("index.html");
|
||||
let html = std::fs::read_to_string(&index_path)
|
||||
.with_context(|| format!("Failed to read {}", index_path.display()))?;
|
||||
info!("Loaded index.html for OG injection");
|
||||
(Some(dist), Some(html))
|
||||
Some(html)
|
||||
} else {
|
||||
info!("No --dist provided, static serving and OG injection disabled");
|
||||
(None, None)
|
||||
info!("No --dist provided; static serving and OG injection disabled");
|
||||
None
|
||||
};
|
||||
|
||||
let http_client = reqwest::Client::new();
|
||||
let http_client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(30))
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
info!("Screenshot service configured: {}", cli.screenshot_url);
|
||||
|
||||
|
|
@ -247,23 +282,46 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
info!("PocketBase configured: {}", cli.pocketbase_url);
|
||||
|
||||
if let (Some(ref email), Some(ref password)) =
|
||||
(&cli.pocketbase_admin_email, &cli.pocketbase_admin_password)
|
||||
{
|
||||
pocketbase::ensure_collections(&http_client, &cli.pocketbase_url, email, password).await?;
|
||||
} else {
|
||||
info!("PocketBase admin credentials not set — skipping collection auto-creation");
|
||||
}
|
||||
pocketbase::ensure_collections(
|
||||
&http_client,
|
||||
&cli.pocketbase_url,
|
||||
&cli.pocketbase_admin_email,
|
||||
&cli.pocketbase_admin_password,
|
||||
)
|
||||
.await?;
|
||||
pocketbase::ensure_oauth_providers(
|
||||
&http_client,
|
||||
&cli.pocketbase_url,
|
||||
&cli.pocketbase_admin_email,
|
||||
&cli.pocketbase_admin_password,
|
||||
&cli.public_url,
|
||||
&cli.google_oauth_client_id,
|
||||
&cli.google_oauth_client_secret,
|
||||
&cli.apple_oauth_client_id,
|
||||
&cli.apple_oauth_client_secret,
|
||||
)
|
||||
.await?;
|
||||
|
||||
info!(
|
||||
"Ollama configured: {} (model: {})",
|
||||
cli.ollama_url, cli.ollama_model
|
||||
);
|
||||
if let Some(ref url) = cli.r5_url {
|
||||
info!("R5 routing service configured: {}", url);
|
||||
} else {
|
||||
info!("R5 routing service not configured (travel time queries disabled)");
|
||||
let tt_path = &cli.travel_times;
|
||||
if !tt_path.exists() {
|
||||
bail!(
|
||||
"Travel times directory not found: {}",
|
||||
tt_path.display()
|
||||
);
|
||||
}
|
||||
info!("Loading travel time data from {}", tt_path.display());
|
||||
let travel_time_store = {
|
||||
let store = data::TravelTimeStore::load(tt_path, 50)?;
|
||||
info!(
|
||||
modes = store.available_modes.len(),
|
||||
"Travel time store loaded"
|
||||
);
|
||||
Arc::new(store)
|
||||
};
|
||||
|
||||
let token_cache = Arc::new(auth::TokenCache::new());
|
||||
|
||||
|
|
@ -286,19 +344,30 @@ async fn main() -> anyhow::Result<()> {
|
|||
index_html,
|
||||
http_client,
|
||||
pocketbase_url: cli.pocketbase_url,
|
||||
pocketbase_admin_email: cli.pocketbase_admin_email,
|
||||
pocketbase_admin_password: cli.pocketbase_admin_password,
|
||||
ollama_url: cli.ollama_url,
|
||||
ollama_model: cli.ollama_model,
|
||||
r5_url: cli.r5_url,
|
||||
travel_time_store,
|
||||
token_cache,
|
||||
ai_filters_schema,
|
||||
ai_filters_system_prompt,
|
||||
google_maps_api_key: cli.google_maps_api_key,
|
||||
stripe_secret_key: cli.stripe_secret_key,
|
||||
stripe_webhook_secret: cli.stripe_webhook_secret,
|
||||
stripe_referral_coupon_id: cli.stripe_referral_coupon_id,
|
||||
});
|
||||
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any);
|
||||
.allow_origin(
|
||||
state
|
||||
.public_url
|
||||
.parse::<axum::http::HeaderValue>()
|
||||
.expect("public_url must be a valid header value"),
|
||||
)
|
||||
.allow_methods(AllowMethods::mirror_request())
|
||||
.allow_headers(AllowHeaders::mirror_request())
|
||||
.allow_credentials(true);
|
||||
|
||||
let state_features = state.clone();
|
||||
let state_hexagons = state.clone();
|
||||
|
|
@ -319,6 +388,15 @@ async fn main() -> anyhow::Result<()> {
|
|||
let state_short_url = state.clone();
|
||||
let state_ai_filters = state.clone();
|
||||
let state_streetview = state.clone();
|
||||
let state_subscription = state.clone();
|
||||
let state_newsletter = state.clone();
|
||||
let state_travel_modes = state.clone();
|
||||
let state_checkout = state.clone();
|
||||
let state_stripe_webhook = state.clone();
|
||||
let state_pricing = state.clone();
|
||||
let state_invites_create = state.clone();
|
||||
let state_invite_get = state.clone();
|
||||
let state_redeem_invite = state.clone();
|
||||
|
||||
let api = Router::new()
|
||||
.route(
|
||||
|
|
@ -327,11 +405,11 @@ async fn main() -> anyhow::Result<()> {
|
|||
)
|
||||
.route(
|
||||
"/api/hexagons",
|
||||
get(move |query| routes::get_hexagons(state_hexagons.clone(), query)),
|
||||
get(move |ext, query| routes::get_hexagons(state_hexagons.clone(), ext, query)),
|
||||
)
|
||||
.route(
|
||||
"/api/postcodes",
|
||||
get(move |query| routes::get_postcodes(state_postcodes.clone(), query)),
|
||||
get(move |ext, query| routes::get_postcodes(state_postcodes.clone(), ext, query)),
|
||||
)
|
||||
.route(
|
||||
"/api/postcode/{postcode}",
|
||||
|
|
@ -349,19 +427,23 @@ async fn main() -> anyhow::Result<()> {
|
|||
"/api/places",
|
||||
get(move |query| routes::get_places(state_places.clone(), query)),
|
||||
)
|
||||
.route(
|
||||
"/api/travel-modes",
|
||||
get(move || routes::get_travel_modes(state_travel_modes.clone())),
|
||||
)
|
||||
.route(
|
||||
"/api/hexagon-properties",
|
||||
get(move |query| {
|
||||
routes::get_hexagon_properties(state_hexagon_properties.clone(), query)
|
||||
get(move |ext, query| {
|
||||
routes::get_hexagon_properties(state_hexagon_properties.clone(), ext, query)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/hexagon-stats",
|
||||
get(move |query| routes::get_hexagon_stats(state_hexagon_stats.clone(), query)),
|
||||
get(move |ext, query| routes::get_hexagon_stats(state_hexagon_stats.clone(), ext, query)),
|
||||
)
|
||||
.route(
|
||||
"/api/postcode-stats",
|
||||
get(move |query| routes::get_postcode_stats(state_postcode_stats.clone(), query)),
|
||||
get(move |ext, query| routes::get_postcode_stats(state_postcode_stats.clone(), ext, query)),
|
||||
)
|
||||
.route(
|
||||
"/api/screenshot",
|
||||
|
|
@ -369,12 +451,14 @@ async fn main() -> anyhow::Result<()> {
|
|||
)
|
||||
.route(
|
||||
"/api/export",
|
||||
get(move |query| routes::get_export(state_export.clone(), query)),
|
||||
get(move |ext, query| routes::get_export(state_export.clone(), ext, query))
|
||||
.layer(ConcurrencyLimitLayer::new(3)),
|
||||
)
|
||||
.route("/api/me", get(routes::get_me))
|
||||
.route(
|
||||
"/api/area-summary",
|
||||
post(move |body| routes::post_area_summary(state_area_summary.clone(), body)),
|
||||
post(move |body| routes::post_area_summary(state_area_summary.clone(), body))
|
||||
.layer(ConcurrencyLimitLayer::new(5)),
|
||||
)
|
||||
.route(
|
||||
"/api/shorten",
|
||||
|
|
@ -382,12 +466,54 @@ async fn main() -> anyhow::Result<()> {
|
|||
)
|
||||
.route(
|
||||
"/api/ai-filters",
|
||||
post(move |body| routes::post_ai_filters(state_ai_filters.clone(), body)),
|
||||
post(move |body| routes::post_ai_filters(state_ai_filters.clone(), body))
|
||||
.layer(ConcurrencyLimitLayer::new(5)),
|
||||
)
|
||||
.route(
|
||||
"/api/streetview",
|
||||
get(move |query| routes::get_streetview(state_streetview.clone(), query)),
|
||||
)
|
||||
.route(
|
||||
"/api/subscription",
|
||||
patch(move |ext, body| {
|
||||
routes::patch_subscription(state_subscription.clone(), ext, body)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/newsletter",
|
||||
patch(move |ext, body| {
|
||||
routes::patch_newsletter(state_newsletter.clone(), ext, body)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/pricing",
|
||||
get(move || routes::get_pricing(state_pricing.clone())),
|
||||
)
|
||||
.route(
|
||||
"/api/checkout",
|
||||
post(move |ext, body| routes::post_checkout(state_checkout.clone(), ext, body))
|
||||
.layer(ConcurrencyLimitLayer::new(10)),
|
||||
)
|
||||
.route(
|
||||
"/api/stripe-webhook",
|
||||
post(move |headers, body| {
|
||||
routes::post_stripe_webhook(state_stripe_webhook.clone(), headers, body)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/invites",
|
||||
post(move |ext, body| routes::post_invites(state_invites_create.clone(), ext, body)),
|
||||
)
|
||||
.route(
|
||||
"/api/invite/{code}",
|
||||
get(move |ext, path| routes::get_invite(state_invite_get.clone(), ext, path)),
|
||||
)
|
||||
.route(
|
||||
"/api/redeem-invite",
|
||||
post(move |ext, body| {
|
||||
routes::post_redeem_invite(state_redeem_invite.clone(), ext, body)
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/s/{code}",
|
||||
get(move |path| routes::get_short_url(state_short_url.clone(), path)),
|
||||
|
|
@ -396,6 +522,7 @@ async fn main() -> anyhow::Result<()> {
|
|||
// Add tile routes
|
||||
let reader_tile = tile_reader.clone();
|
||||
let reader_style = tile_reader.clone();
|
||||
let public_url_tiles = state.public_url.clone();
|
||||
let api = api
|
||||
.route(
|
||||
"/api/tiles/{z}/{x}/{y}",
|
||||
|
|
@ -403,8 +530,9 @@ async fn main() -> anyhow::Result<()> {
|
|||
)
|
||||
.route(
|
||||
"/api/tiles/style.json",
|
||||
get(move |headers, query| {
|
||||
routes::get_style(axum::extract::State(reader_style.clone()), headers, query)
|
||||
get(move |query| {
|
||||
let pu = public_url_tiles.clone();
|
||||
routes::get_style(axum::extract::State(reader_style.clone()), pu, query)
|
||||
}),
|
||||
)
|
||||
.route("/health", get(|| async { "ok" }))
|
||||
|
|
@ -417,15 +545,13 @@ async fn main() -> anyhow::Result<()> {
|
|||
any(move |req| routes::proxy_to_pocketbase(state_pb.clone(), req)),
|
||||
);
|
||||
|
||||
let app = if let Some(ref dist) = frontend_dist {
|
||||
let app = if let Some(ref dist) = cli.dist {
|
||||
api.fallback_service(
|
||||
ServeDir::new(dist).not_found_service(ServeFile::new(dist.join("index.html"))),
|
||||
)
|
||||
} else {
|
||||
api
|
||||
};
|
||||
|
||||
let app = app
|
||||
}
|
||||
.layer(middleware::from_fn(metrics::track_metrics))
|
||||
.layer(middleware::from_fn(auth::auth_middleware))
|
||||
.layer(middleware::from_fn(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue