Files
OpenFUT-Bridge/src/main.rs
T
funman300 eae3e72fd9 Phase 13: web dashboard at /_bridge/dashboard
Single-page club management UI served by the bridge. The Core URL is
injected server-side so client JS calls Core directly without routing
through the proxy. Vanilla HTML/CSS/JS, dark theme matching admin.html.

Sections: Club (name/manager/coins/stats edit), Collection (filterable
card grid with stat bars + chemistry/training pills), Packs (open with
modal, history), Objectives (progress bars + one-click claim), Division
(W/D/L record, season progress, rivals weekly claim), FUT Champions
(current session with in-browser match simulation, claim, history),
Notifications (badge count in nav).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 17:18:10 -07:00

135 lines
4.6 KiB
Rust

use anyhow::Result;
use axum::{
routing::{any, delete, get, post},
Router,
};
use openfut_bridge::{config::Config, proxy::ProxyState, routes};
use tower_http::{cors::CorsLayer, trace::TraceLayer};
use tracing::info;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
#[tokio::main]
async fn main() -> Result<()> {
dotenvy::dotenv().ok();
tracing_subscriber::registry()
.with(
EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "openfut_bridge=debug,tower_http=info".into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
let cfg = Config::from_env()?;
let listen_addr = cfg.listen_addr.clone();
let tls_enabled = cfg.tls_enabled;
info!("OpenFUT Bridge starting on {listen_addr}");
info!("Core URL: {}", cfg.core_url);
info!("Placeholder mode: {}", cfg.placeholder_mode);
info!("Captures dir: {}", cfg.captures_dir);
info!("TLS enabled: {tls_enabled}");
std::fs::create_dir_all(&cfg.captures_dir)?;
let addr: std::net::SocketAddr = listen_addr.parse()?;
if tls_enabled {
// Generate cert/key before building state so the cert can be served via /_bridge/cert.pem
let (cert_pem, key_pem) = openfut_bridge::tls::generate_self_signed_cert()?;
let state = ProxyState::new(cfg).with_cert(cert_pem.clone());
let app = build_router(state);
serve_tls(app, cert_pem, key_pem, addr).await
} else {
let state = ProxyState::new(cfg);
let app = build_router(state);
info!("Bridge listening on http://{addr}");
info!("Set TLS_ENABLED=true to serve over HTTPS");
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
}
fn build_router(state: ProxyState) -> Router {
Router::new()
.route("/_bridge/health", get(routes::health::get_health))
.route("/_bridge/cert.pem", get(routes::health::get_cert))
.route("/_bridge/guide", get(routes::health::get_guide))
.route("/_bridge/dashboard", get(routes::health::get_dashboard))
.route("/_bridge/admin", get(routes::admin::get_admin_dashboard))
.route("/_bridge/captures", get(routes::admin::get_captures))
.route("/_bridge/captures", delete(routes::admin::delete_captures))
.route(
"/_bridge/captures/stream",
get(routes::admin::get_captures_stream),
)
.route(
"/_bridge/unknown",
get(routes::admin::get_unknown_endpoints),
)
.route("/_bridge/status", get(routes::admin::get_endpoint_status))
.route(
"/_bridge/captures/diff",
get(routes::admin::get_capture_diff),
)
.route(
"/_bridge/captures/:id/replay",
post(routes::admin::post_replay_capture),
)
.fallback(any(openfut_bridge::proxy::catch_all_handler))
.layer(TraceLayer::new_for_http())
.layer(CorsLayer::permissive())
.with_state(state)
}
async fn serve_tls(
app: Router,
cert_pem: Vec<u8>,
key_pem: Vec<u8>,
addr: std::net::SocketAddr,
) -> Result<()> {
use hyper_util::rt::{TokioExecutor, TokioIo};
use openfut_bridge::tls::make_tls_acceptor;
use tower::ServiceExt;
let acceptor = make_tls_acceptor(&cert_pem, &key_pem)?;
info!("Bridge listening on https://{addr} (self-signed TLS)");
info!("Download the CA cert from /_bridge/cert.pem and install it as trusted");
info!("Setup guide: https://{addr}/_bridge/guide");
let listener = tokio::net::TcpListener::bind(addr).await?;
loop {
let (tcp, _peer) = listener.accept().await?;
let acceptor = acceptor.clone();
let app = app.clone();
tokio::spawn(async move {
let tls_stream = match acceptor.accept(tcp).await {
Ok(s) => s,
Err(e) => {
tracing::warn!("TLS handshake failed: {e}");
return;
}
};
let io = TokioIo::new(tls_stream);
let svc = hyper::service::service_fn(
move |req: hyper::Request<hyper::body::Incoming>| {
let app = app.clone();
async move { app.oneshot(req.map(axum::body::Body::new)).await }
},
);
if let Err(e) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection(io, svc)
.await
{
tracing::debug!("TLS connection closed: {e}");
}
});
}
}