aboutsummaryrefslogtreecommitdiffstats
path: root/crates/server/src/router.rs
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-09 21:43:23 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2026-07-09 21:43:23 +0200
commit3223d93cb3c77ab02aa0a35f2a8314e447cee9a4 (patch)
tree3971a1f37f5fe3cadb747c5229a0d54e868e45e3 /crates/server/src/router.rs
parentfix(client/sync): Pass through precise error on `SyncError::WrongKey` (diff)
downloadatuin-3223d93cb3c77ab02aa0a35f2a8314e447cee9a4.zip
chore: Separate daemon, client, server, and lib into crates
Diffstat (limited to 'crates/server/src/router.rs')
-rw-r--r--crates/server/src/router.rs98
1 files changed, 98 insertions, 0 deletions
diff --git a/crates/server/src/router.rs b/crates/server/src/router.rs
new file mode 100644
index 00000000..2a5c5f15
--- /dev/null
+++ b/crates/server/src/router.rs
@@ -0,0 +1,98 @@
+use crate::{
+ atuin_common::api::{ATUIN_CARGO_VERSION, ATUIN_HEADER_VERSION, ErrorResponse},
+ atuin_server::database::{db::ServerPostgres, models::User},
+};
+use axum::{
+ Router,
+ extract::{FromRequestParts, Path, Request},
+ http::{self, request::Parts},
+ middleware::Next,
+ response::{IntoResponse, Response},
+ routing::{get, post},
+};
+use eyre::Result;
+use tower::ServiceBuilder;
+use tower_http::trace::TraceLayer;
+use uuid::Uuid;
+
+use super::handlers;
+use crate::{
+ handlers::{ErrorResponseStatus, RespExt},
+ metrics,
+ settings::Settings,
+};
+
+pub(crate) struct UserAuth(pub(crate) User);
+
+impl FromRequestParts<AppState> for UserAuth {
+ type Rejection = ErrorResponseStatus<'static>;
+
+ async fn from_request_parts(
+ req: &mut Parts,
+ state: &AppState,
+ ) -> Result<Self, Self::Rejection> {
+ let user_id = {
+ let Path(user_id) =
+ <Path<Uuid> as FromRequestParts<AppState>>::from_request_parts(req, state)
+ .await
+ .map_err(|_| {
+ ErrorResponse::reply("invalid user_id path param")
+ .with_status(http::StatusCode::BAD_REQUEST)
+ })?;
+
+ user_id
+ };
+
+ let user = User { id: user_id };
+
+ Ok(Self(user))
+ }
+}
+
+async fn teapot() -> impl IntoResponse {
+ // This used to return 418: 🫖
+ // Much as it was fun, it wasn't as useful or informative as it should be
+ (http::StatusCode::NOT_FOUND, "404 not found")
+}
+
+/// Ensure that we only try and sync with clients on the same major version
+async fn semver(request: Request, next: Next) -> Response {
+ let mut response = next.run(request).await;
+ response
+ .headers_mut()
+ .insert(ATUIN_HEADER_VERSION, ATUIN_CARGO_VERSION.parse().unwrap());
+
+ response
+}
+
+#[derive(Clone)]
+pub(crate) struct AppState {
+ pub(crate) database: ServerPostgres,
+ pub(crate) settings: Settings,
+}
+
+pub(crate) fn router(database: ServerPostgres, settings: Settings) -> Router {
+ let routes = Router::new()
+ .route("/", get(handlers::index))
+ .route("/api/v0/{user_id}/record", post(handlers::v0::record::post))
+ .route("/api/v0/{user_id}/record", get(handlers::v0::record::index))
+ .route(
+ "/api/v0/{user_id}/record/next",
+ get(handlers::v0::record::next),
+ );
+
+ let path = settings.path.as_str();
+ if path.is_empty() {
+ routes
+ } else {
+ Router::new().nest(path, routes)
+ }
+ .fallback(teapot)
+ .with_state(AppState { database, settings })
+ .layer(
+ ServiceBuilder::new()
+ .layer(TraceLayer::new_for_http())
+ .layer(axum::middleware::from_fn(metrics::track_metrics))
+ .layer(axum::middleware::from_fn(semver)),
+ )
+}