2025-09-14 16:19:44 -04:00
|
|
|
//! 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.
|
2025-08-13 18:52:37 -07:00
|
|
|
|
2025-09-14 16:19:44 -04:00
|
|
|
use std::net::SocketAddr;
|
2025-08-13 18:52:37 -07:00
|
|
|
|
2025-09-14 16:19:44 -04:00
|
|
|
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;
|
2025-09-23 13:08:51 -07:00
|
|
|
use crate::{app::App, settings::Settings};
|
2025-09-14 16:19:44 -04:00
|
|
|
|
|
|
|
|
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<Settings>| 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<SocketAddr>,
|
|
|
|
|
) -> 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 {}
|
2025-08-13 18:52:37 -07:00
|
|
|
}
|