From f25101eef4fa3e8dc309f959e4e2d50acb2ffab9 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Thu, 5 Feb 2026 19:05:03 +0000 Subject: [PATCH] refactor(routes): wire up api/app module structure Create api/mod.rs with API router and webauthn router construction. Create app/mod.rs with page router and static asset handlers. Rewrite routes/mod.rs to combine api::router() + app::router(). Widen support.rs load_*_options visibility to pub(in crate::application::routes). --- src/application/routes/api/mod.rs | 103 ++++++++++++++++++ src/application/routes/app/mod.rs | 75 +++++++++++++ src/application/routes/mod.rs | 171 ++---------------------------- src/application/routes/support.rs | 10 +- 4 files changed, 192 insertions(+), 167 deletions(-) create mode 100644 src/application/routes/api/mod.rs create mode 100644 src/application/routes/app/mod.rs diff --git a/src/application/routes/api/mod.rs b/src/application/routes/api/mod.rs new file mode 100644 index 0000000..8af521d --- /dev/null +++ b/src/application/routes/api/mod.rs @@ -0,0 +1,103 @@ +pub(in crate::application::routes) mod account; +pub(in crate::application::routes) mod backup; +pub(in crate::application::routes) mod bags; +pub(in crate::application::routes) mod brews; +pub(in crate::application::routes) mod cafes; +pub(in crate::application::routes) mod checkin; +pub(in crate::application::routes) mod cups; +pub(in crate::application::routes) mod gear; +mod macros; +pub(in crate::application::routes) mod roasters; +pub(in crate::application::routes) mod roasts; +pub(in crate::application::routes) mod scan; +pub(in crate::application::routes) mod tokens; +pub(in crate::application::routes) mod webauthn; + +use axum::extract::DefaultBodyLimit; +use axum::routing::{get, post}; + +use crate::application::server::AppState; + +#[allow(clippy::too_many_lines)] +pub(super) fn router() -> axum::Router { + axum::Router::new() + .route( + "/roasters", + get(roasters::list_roasters).post(roasters::create_roaster), + ) + .route( + "/roasters/:id", + get(roasters::get_roaster) + .put(roasters::update_roaster) + .delete(roasters::delete_roaster), + ) + .route( + "/roasts", + get(roasts::list_roasts).post(roasts::create_roast), + ) + .route( + "/roasts/:id", + get(roasts::get_roast) + .put(roasts::update_roast) + .delete(roasts::delete_roast), + ) + .route("/bags", get(bags::list_bags).post(bags::create_bag)) + .route( + "/bags/:id", + get(bags::get_bag) + .put(bags::update_bag) + .delete(bags::delete_bag), + ) + .route("/gear", get(gear::list_gear).post(gear::create_gear)) + .route( + "/gear/:id", + get(gear::get_gear) + .put(gear::update_gear) + .delete(gear::delete_gear), + ) + .route("/brews", get(brews::list_brews).post(brews::create_brew)) + .route( + "/brews/:id", + get(brews::get_brew).delete(brews::delete_brew), + ) + .route("/cafes", get(cafes::list_cafes).post(cafes::create_cafe)) + .route( + "/cafes/:id", + get(cafes::get_cafe) + .put(cafes::update_cafe) + .delete(cafes::delete_cafe), + ) + .route("/nearby-cafes", get(cafes::nearby_cafes)) + .route("/extract-roaster", post(roasters::extract_roaster)) + .route("/extract-roast", post(roasts::extract_roast_info)) + .route("/extract-bag-scan", post(scan::extract_bag_scan)) + .route("/scan", post(scan::submit_scan)) + .route("/check-in", post(checkin::submit_checkin)) + .route("/cups", get(cups::list_cups).post(cups::create_cup)) + .route("/cups/:id", get(cups::get_cup).delete(cups::delete_cup)) + .route( + "/tokens", + post(tokens::create_token).get(tokens::list_tokens), + ) + .route("/tokens/:id/revoke", post(tokens::revoke_token)) + .route("/passkeys", get(account::list_passkeys)) + .route( + "/passkeys/:id", + axum::routing::delete(account::delete_passkey), + ) + .route("/backup", get(backup::export_backup)) + .route( + "/backup/restore", + post(backup::restore_backup).layer(DefaultBodyLimit::max(50 * 1024 * 1024)), + ) +} + +pub(super) fn webauthn_router() -> axum::Router { + axum::Router::new() + .route("/register/start", post(webauthn::register_start)) + .route("/register/finish", post(webauthn::register_finish)) + .route("/auth/start", get(webauthn::auth_start)) + .route("/auth/finish", post(webauthn::auth_finish)) + .route("/passkey/start", post(webauthn::passkey_add_start)) + .route("/passkey/finish", post(webauthn::passkey_add_finish)) +} diff --git a/src/application/routes/app/mod.rs b/src/application/routes/app/mod.rs new file mode 100644 index 0000000..8c816c8 --- /dev/null +++ b/src/application/routes/app/mod.rs @@ -0,0 +1,75 @@ +mod account; +mod add; +pub(super) mod auth; +mod checkin; +mod data; +mod home; +mod timeline; +mod webauthn; + +use axum::response::{IntoResponse, Redirect}; +use axum::routing::{get, post}; + +use crate::application::server::AppState; + +pub(super) fn router() -> axum::Router { + axum::Router::new() + .route("/", get(home::home_page)) + .route("/login", get(auth::login_page)) + .route("/logout", post(auth::logout)) + .route("/account", get(account::account_page)) + .route("/register/:token", get(webauthn::register_page)) + .route("/auth/cli-callback", get(webauthn::cli_callback_page)) + .route("/data", get(data::data_page)) + .route("/add", get(add::add_page)) + .route("/scan", get(scan_redirect)) + .route("/check-in", get(checkin::checkin_page)) + .route("/timeline", get(timeline::timeline_page)) + .route("/styles.css", get(styles)) + .route("/webauthn.js", get(webauthn_js)) + .route("/components/photo-capture.js", get(photo_capture_js)) + .route( + "/components/searchable-select.js", + get(searchable_select_js), + ) + .route("/favicon.ico", get(favicon)) +} + +async fn scan_redirect() -> Redirect { + Redirect::permanent("/") +} + +async fn styles() -> impl IntoResponse { + ( + [("content-type", "text/css; charset=utf-8")], + include_str!("../../../../static/css/styles.css"), + ) +} + +async fn webauthn_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript; charset=utf-8")], + include_str!("../../../../static/js/webauthn.js"), + ) +} + +async fn photo_capture_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript; charset=utf-8")], + include_str!("../../../../static/js/components/photo-capture.js"), + ) +} + +async fn searchable_select_js() -> impl IntoResponse { + ( + [("content-type", "application/javascript; charset=utf-8")], + include_str!("../../../../static/js/components/searchable-select.js"), + ) +} + +async fn favicon() -> impl IntoResponse { + ( + [("content-type", "image/x-icon")], + include_bytes!("../../../../static/favicon.ico").as_ref(), + ) +} diff --git a/src/application/routes/mod.rs b/src/application/routes/mod.rs index 4c6aa0f..dc98432 100644 --- a/src/application/routes/mod.rs +++ b/src/application/routes/mod.rs @@ -1,31 +1,12 @@ -pub mod account; -pub mod add; -pub mod auth; -pub mod backup; -pub mod bags; -pub mod brews; -pub mod cafes; -pub mod checkin; -pub mod cups; -pub mod data; -pub mod gear; -pub mod home; -mod macros; -pub mod roasters; -pub mod roasts; -pub mod scan; +pub mod api; +pub mod app; pub mod support; -pub mod timeline; -pub mod tokens; -pub mod webauthn; -pub(crate) use auth::is_authenticated; +pub(crate) use app::auth::is_authenticated; use askama::Template; -use axum::extract::DefaultBodyLimit; use axum::http::StatusCode; -use axum::response::{Html, IntoResponse, Redirect}; -use axum::routing::{get, post}; +use axum::response::Html; use tower::ServiceBuilder; use tower_cookies::CookieManagerLayer; use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer}; @@ -36,110 +17,11 @@ use crate::application::server::AppState; use crate::presentation::web::templates::render_template; -#[allow(clippy::too_many_lines)] pub fn app_router(state: AppState) -> axum::Router { - let api_routes = axum::Router::new() - // Public API routes - .route( - "/roasters", - get(roasters::list_roasters).post(roasters::create_roaster), - ) - .route( - "/roasters/:id", - get(roasters::get_roaster) - .put(roasters::update_roaster) - .delete(roasters::delete_roaster), - ) - .route( - "/roasts", - get(roasts::list_roasts).post(roasts::create_roast), - ) - .route( - "/roasts/:id", - get(roasts::get_roast) - .put(roasts::update_roast) - .delete(roasts::delete_roast), - ) - .route("/bags", get(bags::list_bags).post(bags::create_bag)) - .route( - "/bags/:id", - get(bags::get_bag) - .put(bags::update_bag) - .delete(bags::delete_bag), - ) - .route("/gear", get(gear::list_gear).post(gear::create_gear)) - .route( - "/gear/:id", - get(gear::get_gear) - .put(gear::update_gear) - .delete(gear::delete_gear), - ) - .route("/brews", get(brews::list_brews).post(brews::create_brew)) - .route( - "/brews/:id", - get(brews::get_brew).delete(brews::delete_brew), - ) - .route("/cafes", get(cafes::list_cafes).post(cafes::create_cafe)) - .route( - "/cafes/:id", - get(cafes::get_cafe) - .put(cafes::update_cafe) - .delete(cafes::delete_cafe), - ) - .route("/nearby-cafes", get(cafes::nearby_cafes)) - .route("/extract-roaster", post(roasters::extract_roaster)) - .route("/extract-roast", post(roasts::extract_roast_info)) - .route("/extract-bag-scan", post(scan::extract_bag_scan)) - .route("/scan", post(scan::submit_scan)) - .route("/check-in", post(checkin::submit_checkin)) - .route("/cups", get(cups::list_cups).post(cups::create_cup)) - .route("/cups/:id", get(cups::get_cup).delete(cups::delete_cup)) - .route( - "/tokens", - post(tokens::create_token).get(tokens::list_tokens), - ) - .route("/tokens/:id/revoke", post(tokens::revoke_token)) - .route("/passkeys", get(account::list_passkeys)) - .route( - "/passkeys/:id", - axum::routing::delete(account::delete_passkey), - ) - .route("/backup", get(backup::export_backup)) - .route( - "/backup/restore", - post(backup::restore_backup).layer(DefaultBodyLimit::max(50 * 1024 * 1024)), - ); - - let webauthn_routes = axum::Router::new() - .route("/register/start", post(webauthn::register_start)) - .route("/register/finish", post(webauthn::register_finish)) - .route("/auth/start", get(webauthn::auth_start)) - .route("/auth/finish", post(webauthn::auth_finish)) - .route("/passkey/start", post(webauthn::passkey_add_start)) - .route("/passkey/finish", post(webauthn::passkey_add_finish)); - axum::Router::new() - .route("/", get(home::home_page)) - .route("/login", get(auth::login_page)) - .route("/logout", post(auth::logout)) - .route("/account", get(account::account_page)) - .route("/register/:token", get(webauthn::register_page)) - .route("/auth/cli-callback", get(webauthn::cli_callback_page)) - .route("/data", get(data::data_page)) - .route("/add", get(add::add_page)) - .route("/scan", get(scan_redirect)) - .route("/check-in", get(checkin::checkin_page)) - .route("/timeline", get(timeline::timeline_page)) - .route("/styles.css", get(styles)) - .route("/webauthn.js", get(webauthn_js)) - .route("/components/photo-capture.js", get(photo_capture_js)) - .route( - "/components/searchable-select.js", - get(searchable_select_js), - ) - .route("/favicon.ico", get(favicon)) - .nest("/api/v1", api_routes) - .nest("/api/v1/webauthn", webauthn_routes) + .merge(app::router()) + .nest("/api/v1", api::router()) + .nest("/api/v1/webauthn", api::webauthn_router()) .layer( ServiceBuilder::new() .layer( @@ -152,45 +34,6 @@ pub fn app_router(state: AppState) -> axum::Router { .with_state(state) } -async fn scan_redirect() -> Redirect { - Redirect::permanent("/") -} - -async fn styles() -> impl IntoResponse { - ( - [("content-type", "text/css; charset=utf-8")], - include_str!("../../../static/css/styles.css"), - ) -} - -async fn webauthn_js() -> impl IntoResponse { - ( - [("content-type", "application/javascript; charset=utf-8")], - include_str!("../../../static/js/webauthn.js"), - ) -} - -async fn photo_capture_js() -> impl IntoResponse { - ( - [("content-type", "application/javascript; charset=utf-8")], - include_str!("../../../static/js/components/photo-capture.js"), - ) -} - -async fn searchable_select_js() -> impl IntoResponse { - ( - [("content-type", "application/javascript; charset=utf-8")], - include_str!("../../../static/js/components/searchable-select.js"), - ) -} - -async fn favicon() -> impl IntoResponse { - ( - [("content-type", "image/x-icon")], - include_bytes!("../../../static/favicon.ico").as_ref(), - ) -} - pub(crate) fn render_html(template: T) -> Result, StatusCode> { render_template(template).map(Html).map_err(|err| { error!(error = %err, "failed to render template"); diff --git a/src/application/routes/support.rs b/src/application/routes/support.rs index 6183fbe..889654c 100644 --- a/src/application/routes/support.rs +++ b/src/application/routes/support.rs @@ -215,7 +215,7 @@ where } } -pub(super) async fn load_roaster_options( +pub(in crate::application::routes) async fn load_roaster_options( state: &AppState, ) -> Result, AppError> { use crate::domain::roasters::RoasterSortKey; @@ -227,12 +227,16 @@ pub(super) async fn load_roaster_options( Ok(roasters.into_iter().map(RoasterOptionView::from).collect()) } -pub(super) async fn load_roast_options(state: &AppState) -> Result, AppError> { +pub(in crate::application::routes) async fn load_roast_options( + state: &AppState, +) -> Result, AppError> { let roasts = state.roast_repo.list_all().await.map_err(AppError::from)?; Ok(roasts.into_iter().map(RoastOptionView::from).collect()) } -pub(super) async fn load_cafe_options(state: &AppState) -> Result, AppError> { +pub(in crate::application::routes) async fn load_cafe_options( + state: &AppState, +) -> Result, AppError> { use crate::domain::cafes::CafeSortKey; let cafes = state .cafe_repo