//! Hierarchical HTTP routing. //! //! Top level module establishes the overall //! [`axum::Router`], and submodules organize nested subrouters into manageable //! chunks. Pragmatically, the submodule tree should be kept fairly flat, lest //! file paths grow exceedingly long. Deeply nested routers may still be //! implemented, by use of the `super` keyword. use std::net::SocketAddr; use axum::{ Router, extract::{ConnectInfo, State, WebSocketUpgrade, ws::WebSocket}, http::{HeaderValue, header::CACHE_CONTROL}, response::{Redirect, Response}, routing::{any, get}, }; use tower::ServiceBuilder; use tower_http::{ services::{ServeDir, ServeFile}, set_header::SetResponseHeaderLayer, }; use crate::auth; use crate::{app_state::App, settings::Settings}; mod relations_single; mod workspaces_multi; mod workspaces_single; /// Create the root [`Router`] for the application, including nesting according /// to the `root_path` [`crate::settings::Settings`] value, setting cache /// headers, setting up static file handling, and defining fallback handlers. pub(crate) fn new_router(app: App) -> Router<()> { let root_path = app.settings.root_path.clone(); let router = Router::new() .route( "/", get( |State(Settings { root_path, .. }): State| async move { Redirect::to(&format!("{root_path}/workspaces/list/")) }, ), ) .nest("/workspaces", workspaces_multi::new_router()) .nest("/w/{workspace_id}", workspaces_single::new_router()) .nest("/auth", auth::new_router()) .route("/__dev-healthz", any(dev_healthz_handler)) .layer(SetResponseHeaderLayer::if_not_present( CACHE_CONTROL, HeaderValue::from_static("no-cache"), )) .nest_service( "/js_dist", ServiceBuilder::new() .layer(SetResponseHeaderLayer::if_not_present( CACHE_CONTROL, // FIXME: restore production value // HeaderValue::from_static("max-age=21600, stale-while-revalidate=86400"), HeaderValue::from_static("no-cache"), )) .service( ServeDir::new("js_dist").not_found_service( ServiceBuilder::new() .layer(SetResponseHeaderLayer::if_not_present( CACHE_CONTROL, HeaderValue::from_static("no-cache"), )) .service(ServeFile::new("static/_404.html")), ), ), ) .nest_service( "/css_dist", ServiceBuilder::new() .layer(SetResponseHeaderLayer::if_not_present( CACHE_CONTROL, // FIXME: restore production value // HeaderValue::from_static("max-age=21600, stale-while-revalidate=86400"), HeaderValue::from_static("no-cache"), )) .service( ServeDir::new("css_dist").not_found_service( ServiceBuilder::new() .layer(SetResponseHeaderLayer::if_not_present( CACHE_CONTROL, HeaderValue::from_static("no-cache"), )) .service(ServeFile::new("static/_404.html")), ), ), ) .fallback_service( ServiceBuilder::new() .layer(SetResponseHeaderLayer::if_not_present( CACHE_CONTROL, HeaderValue::from_static("max-age=21600, stale-while-revalidate=86400"), )) .service( ServeDir::new("static").not_found_service( ServiceBuilder::new() .layer(SetResponseHeaderLayer::if_not_present( CACHE_CONTROL, HeaderValue::from_static("no-cache"), )) .service(ServeFile::new("static/_404.html")), ), ), ) .with_state(app); if root_path.is_empty() { router } else { Router::new() .nest(&root_path, router) .fallback(|| async move { Redirect::to(&root_path) }) } } /// Development endpoint helping to implement home-grown "hot" reloads. async fn dev_healthz_handler( ws: WebSocketUpgrade, ConnectInfo(addr): ConnectInfo, ) -> Response { tracing::info!("{addr} connected"); ws.on_upgrade(move |socket| handle_dev_healthz_socket(socket, addr)) } async fn handle_dev_healthz_socket(mut socket: WebSocket, _: SocketAddr) { // Keep socket open indefinitely until the entire server exits while let Some(Ok(_)) = socket.recv().await {} }