diff --git a/Cargo.lock b/Cargo.lock index 2f5b860f..a4002ffd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -469,16 +469,45 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.4.5", "bytes", "futures-util", "http 1.2.0", "http-body", "http-body-util", + "itoa", + "matchit 0.7.3", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "sync_wrapper", + "tower 0.5.2", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8" +dependencies = [ + "axum-core 0.5.0", + "bytes", + "form_urlencoded", + "futures-util", + "http 1.2.0", + "http-body", + "http-body-util", "hyper", "hyper-util", "itoa", - "matchit", + "matchit 0.8.4", "memchr", "mime", "multer", @@ -499,11 +528,11 @@ dependencies = [ [[package]] name = "axum-client-ip" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac" +checksum = "dff8ee1869817523c8f91c20bf17fd932707f66c2e7e0b0f811b29a227289562" dependencies = [ - "axum", + "axum 0.8.1", "forwarded-header-value", "serde", ] @@ -526,17 +555,14 @@ dependencies = [ "sync_wrapper", "tower-layer", "tower-service", - "tracing", ] [[package]] -name = "axum-extra" -version = "0.9.6" +name = "axum-core" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04" +checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733" dependencies = [ - "axum", - "axum-core", "bytes", "futures-util", "http 1.2.0", @@ -544,7 +570,29 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "prost 0.12.6", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b" +dependencies = [ + "axum 0.8.1", + "axum-core 0.5.0", + "bytes", + "futures-util", + "http 1.2.0", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "prost", "serde", "tower 0.5.2", "tower-layer", @@ -553,14 +601,14 @@ dependencies = [ [[package]] name = "axum-test" -version = "16.4.1" +version = "17.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e3a443d2608936a02a222da7b746eb412fede7225b3030b64fe9be99eab8dc" +checksum = "53f1009889890a439cbf67a4071a2593d027c65209da4faeac5582f28ca9e6c3" dependencies = [ "anyhow", "assert-json-diff", "auto-future", - "axum", + "axum 0.8.1", "bytes", "bytesize", "cookie", @@ -1252,7 +1300,7 @@ dependencies = [ name = "custom-binary" version = "0.1.0" dependencies = [ - "axum", + "axum 0.8.1", "env_logger", "tokio", "tracing-subscriber", @@ -2891,15 +2939,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -3168,6 +3207,12 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "maxminddb" version = "0.24.0" @@ -3575,7 +3620,7 @@ dependencies = [ "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", - "prost 0.13.4", + "prost", "serde_json", "thiserror 1.0.69", "tokio", @@ -3592,7 +3637,7 @@ dependencies = [ "hex", "opentelemetry", "opentelemetry_sdk", - "prost 0.13.4", + "prost", "serde", "tonic", ] @@ -4019,16 +4064,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes", - "prost-derive 0.12.6", -] - [[package]] name = "prost" version = "0.13.4" @@ -4036,7 +4071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c0fef6c4230e4ccf618a35c59d7ede15dea37de8427500f50aff708806e42ec" dependencies = [ "bytes", - "prost-derive 0.13.4", + "prost-derive", ] [[package]] @@ -4052,26 +4087,13 @@ dependencies = [ "once_cell", "petgraph", "prettyplease", - "prost 0.13.4", - "prost-types 0.13.4", + "prost", + "prost-types", "regex", "syn 2.0.95", "tempfile", ] -[[package]] -name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.95", -] - [[package]] name = "prost-derive" version = "0.13.4" @@ -4085,29 +4107,17 @@ dependencies = [ "syn 2.0.95", ] -[[package]] -name = "prost-reflect" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5eec97d5d34bdd17ad2db2219aabf46b054c6c41bd5529767c9ce55be5898f" -dependencies = [ - "logos", - "once_cell", - "prost 0.12.6", - "prost-reflect-derive 0.13.0", - "prost-types 0.12.6", -] - [[package]] name = "prost-reflect" version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20ae544fca2892fd4b7e9ff26cba1090cedf1d4d95c2aded1af15d2f93f270b8" dependencies = [ + "logos", "once_cell", - "prost 0.13.4", - "prost-reflect-derive 0.14.0", - "prost-types 0.13.4", + "prost", + "prost-reflect-derive", + "prost-types", ] [[package]] @@ -4117,18 +4127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50e2537231d94dd2778920c2ada37dd9eb1ac0325bb3ee3ee651bd44c1134123" dependencies = [ "prost-build", - "prost-reflect 0.14.3", -] - -[[package]] -name = "prost-reflect-derive" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c3f519df051f8a700c5aa42b53f9c42d54959506b7ed58ac7a6af7991fdc22" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.95", + "prost-reflect", ] [[package]] @@ -4142,22 +4141,13 @@ dependencies = [ "syn 2.0.95", ] -[[package]] -name = "prost-types" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" -dependencies = [ - "prost 0.12.6", -] - [[package]] name = "prost-types" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc2f1e56baa61e93533aebc21af4d2134b70f66275e0fcdf3cbe43d77ff7e8fc" dependencies = [ - "prost 0.13.4", + "prost", ] [[package]] @@ -5951,7 +5941,7 @@ checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.7.9", "base64 0.22.1", "bytes", "h2", @@ -5963,7 +5953,7 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost 0.13.4", + "prost", "socket2", "tokio", "tokio-stream", @@ -6011,12 +6001,11 @@ dependencies = [ [[package]] name = "tower-cookies" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fd0118512cf0b3768f7fcccf0bef1ae41d68f2b45edc1e77432b36c97c56c6d" +checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36" dependencies = [ - "async-trait", - "axum-core", + "axum-core 0.5.0", "cookie", "futures-util", "http 1.2.0", @@ -6132,7 +6121,7 @@ dependencies = [ "argon2", "async-channel 2.3.1", "async-trait", - "axum", + "axum 0.8.1", "axum-client-ip", "axum-extra", "axum-test", @@ -6158,9 +6147,9 @@ dependencies = [ "oauth2", "object_store", "parking_lot", - "prost 0.12.6", + "prost", "prost-build", - "prost-reflect 0.13.1", + "prost-reflect", "prost-reflect-build", "quoted_printable", "rand", @@ -6202,7 +6191,7 @@ dependencies = [ name = "trailbase-cli" version = "0.2.0" dependencies = [ - "axum", + "axum 0.8.1", "chrono", "clap", "env_logger", @@ -6524,7 +6513,7 @@ version = "8.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db4b5ac679cc6dfc5ea3f2823b0291c777750ffd5e13b21137e0f7ac0e8f9617" dependencies = [ - "axum", + "axum 0.7.9", "base64 0.22.1", "mime_guess", "regex", diff --git a/client/testfixture/scripts/index.ts b/client/testfixture/scripts/index.ts index 3e821a1e..4627d6e6 100644 --- a/client/testfixture/scripts/index.ts +++ b/client/testfixture/scripts/index.ts @@ -33,7 +33,7 @@ addRoute( addRoute( "GET", - "/test/:table", + "/test/{table}", stringHandler(async (req: StringRequestType) => { const table = req.params["table"]; if (table) { diff --git a/examples/custom-binary/Cargo.toml b/examples/custom-binary/Cargo.toml index 01a17bcc..5a20e7d6 100644 --- a/examples/custom-binary/Cargo.toml +++ b/examples/custom-binary/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" publish = false [dependencies] -axum = { version = "^0.7.5" } +axum = "^0.8.1" env_logger = "^0.11.3" tokio = { version = "^1.38.0", features=["macros", "rt-multi-thread"] } tracing-subscriber = "0.3.18" diff --git a/trailbase-cli/Cargo.toml b/trailbase-cli/Cargo.toml index 4c1ae189..e07b2fc8 100644 --- a/trailbase-cli/Cargo.toml +++ b/trailbase-cli/Cargo.toml @@ -12,7 +12,7 @@ name = "trail" openapi = ["dep:utoipa", "dep:utoipa-swagger-ui"] [dependencies] -axum = { version = "^0.7.5", features=["multipart"] } +axum = { version = "^0.8.1", features=["multipart"] } chrono = "^0.4.38" clap = { version = "^4.4.11", features=["derive", "env"] } env_logger = "^0.11.3" diff --git a/trailbase-core/Cargo.toml b/trailbase-core/Cargo.toml index 2ce4df15..cbd85d7a 100644 --- a/trailbase-core/Cargo.toml +++ b/trailbase-core/Cargo.toml @@ -27,9 +27,9 @@ arc-swap = "1.7.1" argon2 = { version = "^0.5.3", default-features = false, features = ["alloc", "password-hash"] } async-channel = "2.3.1" async-trait = "0.1.80" -axum = { version = "^0.7.5", features = ["multipart"] } -axum-client-ip = "0.6.0" -axum-extra = { version = "^0.9.3", default-features = false, features = ["protobuf"] } +axum = { version = "^0.8.1", features = ["multipart"] } +axum-client-ip = "0.7.0" +axum-extra = { version = "^0.10.0", default-features = false, features = ["protobuf"] } base64 = { version = "0.22.1", default-features = false } bytes = { version = "1.8.0", features = ["serde"] } chrono = "^0.4.38" @@ -51,8 +51,8 @@ minijinja = "2.1.2" oauth2 = { version = "5.0.0-alpha.4", default-features = false, features = ["reqwest", "rustls-tls"] } object_store = { version = "0.11.0", default-features = false, features = ["aws"] } parking_lot = "0.12.3" -prost = "^0.12.6" -prost-reflect = { version = "^0.13.0", features = ["derive", "text-format"] } +prost = "^0.13.4" +prost-reflect = { version = "^0.14.3", features = ["derive", "text-format"] } rand = "0.8.5" trailbase-refinery-core = { workspace = true } trailbase-refinery-macros = { workspace = true } @@ -71,7 +71,7 @@ sqlite3-parser = "0.13.0" thiserror = "2.0.1" thread_local = "1.1.8" tokio = { version = "^1.38.0", features = ["macros", "rt-multi-thread", "fs", "signal", "time"] } -tower-cookies = { version = "0.10.0" } +tower-cookies = "0.11.0" tower-http = { version = "^0.6.0", features = ["cors", "trace", "fs", "limit"] } tower-service = "0.3.3" tracing = "0.1.40" @@ -91,7 +91,7 @@ prost-reflect-build = "0.14.0" [dev-dependencies] anyhow = "^1.0.86" -axum-test = "16.3.0" +axum-test = "17.0.1" criterion = { version = "0.5", features = ["html_reports", "async_tokio"] } trailbase-extension = { workspace = true } quoted_printable = "0.5.1" diff --git a/trailbase-core/src/admin/mod.rs b/trailbase-core/src/admin/mod.rs index 5ca4b521..e120947f 100644 --- a/trailbase-core/src/admin/mod.rs +++ b/trailbase-core/src/admin/mod.rs @@ -21,19 +21,22 @@ use axum::{ pub fn router() -> Router { Router::new() // Row actions. - .route("/table/:table_name/rows", get(rows::list_rows_handler)) - .route("/table/:table_name/files", get(rows::read_files_handler)) - .route("/table/:table_name/rows", delete(rows::delete_rows_handler)) - .route("/table/:table_name", patch(rows::update_row_handler)) - .route("/table/:table_name", post(rows::insert_row_handler)) - .route("/table/:table_name", delete(rows::delete_row_handler)) + .route("/table/{table_name}/rows", get(rows::list_rows_handler)) + .route("/table/{table_name}/files", get(rows::read_files_handler)) + .route( + "/table/{table_name}/rows", + delete(rows::delete_rows_handler), + ) + .route("/table/{table_name}", patch(rows::update_row_handler)) + .route("/table/{table_name}", post(rows::insert_row_handler)) + .route("/table/{table_name}", delete(rows::delete_row_handler)) // Index actions. .route("/index", post(table::create_index_handler)) .route("/index", patch(table::alter_index_handler)) .route("/index", delete(table::drop_index_handler)) // Table actions. .route( - "/table/:table_name/schema.json", + "/table/{table_name}/schema.json", get(table::get_table_schema_handler), ) .route("/table", post(table::create_table_handler)) diff --git a/trailbase-core/src/auth/api/avatar.rs b/trailbase-core/src/auth/api/avatar.rs index 41e2e77e..97508c20 100644 --- a/trailbase-core/src/auth/api/avatar.rs +++ b/trailbase-core/src/auth/api/avatar.rs @@ -133,6 +133,7 @@ mod tests { Part::bytes(body_slice.to_vec()).file_name(filename), ); let content_type = form.content_type(); + let body: axum::body::Body = form.into(); http::Request::builder() diff --git a/trailbase-core/src/auth/mod.rs b/trailbase-core/src/auth/mod.rs index 10c0f46c..c63ca486 100644 --- a/trailbase-core/src/auth/mod.rs +++ b/trailbase-core/src/auth/mod.rs @@ -22,6 +22,8 @@ pub use jwt::{JwtHelper, TokenClaims}; pub(crate) use ui::auth_ui_router; pub use user::User; +use crate::constants::AUTH_API_PATH; + #[derive(OpenApi)] #[openapi( paths( @@ -82,68 +84,101 @@ pub(super) fn router() -> Router { // * vacuum expired pending registrations. return Router::new() // Sign-up new users. - .route("/register", post(api::register::register_user_handler)) + .route( + &format!("/{AUTH_API_PATH}/register"), + post(api::register::register_user_handler), + ) // E-mail verification and change flows. .route( - "/verify_email/trigger", + &format!("/{AUTH_API_PATH}/verify_email/trigger"), get(api::verify_email::request_email_verification_handler), ) .route( - "/verify_email/confirm/:email_verification_code", + &format!("/{AUTH_API_PATH}/verify_email/confirm/{{email_verification_code}}"), get(api::verify_email::verify_email_handler), ) .route( - "/change_email/request", + &format!("/{AUTH_API_PATH}/change_email/request"), post(api::change_email::change_email_request_handler), ) .route( - "/change_email/confirm/:email_verification_code", + &format!("/{AUTH_API_PATH}/change_email/confirm/{{email_verification_code}}"), get(api::change_email::change_email_confirm_handler), ) // Password-reset flow. .route( - "/reset_password/request", + &format!("/{AUTH_API_PATH}/reset_password/request"), post(api::reset_password::reset_password_request_handler), ) .route( - "/reset_password/update/:password_reset_code", + &format!("/{AUTH_API_PATH}/reset_password/update/{{password_reset_code}}"), post(api::reset_password::reset_password_update_handler), ) // Change password flow. .route( - "/change_password", + &format!("/{AUTH_API_PATH}/change_password"), post(api::change_password::change_password_handler), ) // Token refresh flow. - .route("/refresh", post(api::refresh::refresh_handler)) + .route( + &format!("/{AUTH_API_PATH}/refresh"), + post(api::refresh::refresh_handler), + ) // Login - .route("/login", post(api::login::login_handler)) + .route( + &format!("/{AUTH_API_PATH}/login"), + post(api::login::login_handler), + ) // Converts auth code (+pkce code verifier) to auth tokens - .route("/token", post(api::token::auth_code_to_token_handler)) + .route( + &format!("/{AUTH_API_PATH}/token"), + post(api::token::auth_code_to_token_handler), + ) // Login status (also let's one lift tokens from cookies). - .route("/status", get(api::login::login_status_handler)) + .route( + &format!("/{AUTH_API_PATH}/status"), + get(api::login::login_status_handler), + ) // Logout [get]: deletes all sessions for the current user. - .route("/logout", get(api::logout::logout_handler)) + .route( + &format!("/{AUTH_API_PATH}/logout"), + get(api::logout::logout_handler), + ) // Logout [post]: deletes given session - .route("/logout", post(api::logout::post_logout_handler)) + .route( + &format!("/{AUTH_API_PATH}/logout"), + post(api::logout::post_logout_handler), + ) // Get a user's avatar. .route( - "/avatar/:b64_user_id", + &format!("/{AUTH_API_PATH}/avatar/{{b64_user_id}}"), get(api::avatar::get_avatar_url_handler), ) // User delete. - .route("/delete", delete(api::delete::delete_handler)) + .route( + &format!("/{AUTH_API_PATH}/delete"), + delete(api::delete::delete_handler), + ) // OAuth flows: list providers, login+callback - .nest("/oauth", oauth::oauth_router()); + .nest(&format!("/{AUTH_API_PATH}/oauth"), oauth::oauth_router()); } /// Replicating minimal functionality of the above main router in case the admin dash is routed /// from a different port to prevent cross-origin requests. pub(super) fn admin_auth_router() -> Router { return Router::new() - .route("/login", post(api::login::login_handler)) - .route("/status", get(api::login::login_status_handler)) - .route("/logout", get(api::logout::logout_handler)); + .route( + &format!("/{AUTH_API_PATH}/login"), + post(api::login::login_handler), + ) + .route( + &format!("/{AUTH_API_PATH}/status"), + get(api::login::login_status_handler), + ) + .route( + &format!("/{AUTH_API_PATH}/logout"), + get(api::logout::logout_handler), + ); } #[cfg(test)] diff --git a/trailbase-core/src/auth/oauth/mod.rs b/trailbase-core/src/auth/oauth/mod.rs index 184af59d..c5fadb24 100644 --- a/trailbase-core/src/auth/oauth/mod.rs +++ b/trailbase-core/src/auth/oauth/mod.rs @@ -23,11 +23,11 @@ pub fn oauth_router() -> Router { get(list_providers::list_configured_providers_handler), ) .route( - "/:provider/login", + "/{provider}/login", get(login::login_with_external_auth_provider), ) .route( - "/:provider/callback", + "/{provider}/callback", get(callback::callback_from_external_auth_provider), ) } diff --git a/trailbase-core/src/auth/tokens.rs b/trailbase-core/src/auth/tokens.rs index cfb02635..e61ea2ec 100644 --- a/trailbase-core/src/auth/tokens.rs +++ b/trailbase-core/src/auth/tokens.rs @@ -1,6 +1,5 @@ use axum::{ - async_trait, - extract::{FromRef, FromRequestParts}, + extract::{FromRef, FromRequestParts, OptionalFromRequestParts}, http::{header, request::Parts}, }; use chrono::Duration; @@ -25,7 +24,6 @@ pub(crate) struct Tokens { pub refresh_token: Option, } -#[async_trait] impl FromRequestParts for Tokens where AppState: FromRef, @@ -45,6 +43,28 @@ where } } +impl OptionalFromRequestParts for Tokens +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = AuthError; + + async fn from_request_parts( + parts: &mut Parts, + state: &S, + ) -> Result, Self::Rejection> { + let state = AppState::from_ref(state); + + if let Ok(tokens) = extract_tokens_from_headers(&state, &parts.headers).await { + return Ok(Some(tokens)); + } + + let cookies = extract_cookies_from_parts(parts)?; + return Ok(extract_tokens_from_cookies(&state, &cookies).await.ok()); + } +} + async fn extract_tokens_from_headers( state: &AppState, headers: &header::HeaderMap, diff --git a/trailbase-core/src/auth/ui/mod.rs b/trailbase-core/src/auth/ui/mod.rs index 64a1ed03..6ca41a6b 100644 --- a/trailbase-core/src/auth/ui/mod.rs +++ b/trailbase-core/src/auth/ui/mod.rs @@ -233,20 +233,20 @@ pub(crate) fn auth_ui_router() -> Router { ); return Router::new() - .route("/login", get(ui_login_handler)) - .route("/logout", get(ui_logout_handler)) - .route("/register", get(ui_register_handler)) + .route("/_/auth/login", get(ui_login_handler)) + .route("/_/auth/logout", get(ui_logout_handler)) + .route("/_/auth/register", get(ui_register_handler)) .route( - "/reset_password/request", + "/_/auth/reset_password/request", get(ui_reset_password_request_handler), ) .route( - "/reset_password/update", + "/_/auth/reset_password/update", get(ui_reset_password_update_handler), ) - .route("/change_password", get(ui_change_password_handler)) - .route("/change_email", get(ui_change_email_handler)) - .nest_service("/", serve_auth_assets); + .route("/_/auth/change_password", get(ui_change_password_handler)) + .route("/_/auth/change_email", get(ui_change_email_handler)) + .nest_service("/_/auth/", serve_auth_assets); } fn hidden_input(name: &str, value: Option<&String>) -> String { diff --git a/trailbase-core/src/auth/user.rs b/trailbase-core/src/auth/user.rs index 69f5b2f7..971b20b2 100644 --- a/trailbase-core/src/auth/user.rs +++ b/trailbase-core/src/auth/user.rs @@ -1,6 +1,5 @@ use axum::{ - async_trait, - extract::{FromRef, FromRequestParts}, + extract::{FromRef, FromRequestParts, OptionalFromRequestParts}, http::request::Parts, }; use serde::{Deserialize, Serialize}; @@ -110,7 +109,6 @@ impl User { } } -#[async_trait] impl FromRequestParts for User where AppState: FromRef, @@ -119,10 +117,30 @@ where type Rejection = AuthError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let tokens = Tokens::from_request_parts(parts, state).await?; + let tokens = >::from_request_parts(parts, state).await?; return User::from_token_claims(tokens.auth_token_claims); } } + +impl OptionalFromRequestParts for User +where + AppState: FromRef, + S: Send + Sync, +{ + type Rejection = AuthError; + + async fn from_request_parts( + parts: &mut Parts, + state: &S, + ) -> Result, Self::Rejection> { + let tokens = >::from_request_parts(parts, state).await?; + if let Some(tokens) = tokens { + return Ok(Some(User::from_token_claims(tokens.auth_token_claims)?)); + } + return Ok(None); + } +} + #[cfg(test)] mod tests { use super::*; @@ -165,6 +183,8 @@ mod tests { .unwrap(); let (mut parts, _body) = request.into_parts(); - User::from_request_parts(&mut parts, &state).await.unwrap(); + >::from_request_parts(&mut parts, &state) + .await + .unwrap(); } } diff --git a/trailbase-core/src/auth/util.rs b/trailbase-core/src/auth/util.rs index 6f40eb6d..0a9318fb 100644 --- a/trailbase-core/src/auth/util.rs +++ b/trailbase-core/src/auth/util.rs @@ -107,24 +107,27 @@ pub(crate) fn remove_all_cookies(cookies: &Cookies) { } } -#[cfg(test)] -pub(crate) fn extract_cookies_from_parts(parts: &mut Parts) -> Result { - let cookies = Cookies::default(); - - for ref header in parts.headers.get_all(axum::http::header::COOKIE) { - cookies.add(Cookie::parse(header.to_str().unwrap().to_string()).unwrap()); - } - - return Ok(cookies); -} - -#[cfg(not(test))] pub(crate) fn extract_cookies_from_parts(parts: &mut Parts) -> Result { if let Some(cookies) = parts.extensions.get::() { return Ok(cookies.clone()); }; - log::error!("Failed to get Cookies"); - return Err(AuthError::Internal("cookie error".into())); + + // Fallback code for when handlers are called directly in unit tests w/o tower::Cookies + // middleware to parse the cookies header for us. + #[cfg(test)] + { + let cookies = Cookies::default(); + for ref header in parts.headers.get_all(axum::http::header::COOKIE) { + cookies.add(Cookie::parse(header.to_str().unwrap().to_string()).unwrap()); + } + return Ok(cookies); + } + + #[cfg(not(test))] + { + log::error!("Failed to get Cookies"); + return Err(AuthError::Internal("cookie error".into())); + } } pub async fn user_by_email(state: &AppState, email: &str) -> Result { diff --git a/trailbase-core/src/extract/either.rs b/trailbase-core/src/extract/either.rs index 024f41ef..8cdd59a8 100644 --- a/trailbase-core/src/extract/either.rs +++ b/trailbase-core/src/extract/either.rs @@ -1,4 +1,3 @@ -use axum::async_trait; use axum::extract::{rejection::*, Form, FromRequest, Request}; use axum::http::header::CONTENT_TYPE; use axum::http::StatusCode; @@ -43,7 +42,6 @@ pub enum Either { // Proto(DynamicMessage), } -#[async_trait] impl FromRequest for Either where T: DeserializeOwned + Sync + Send + 'static, diff --git a/trailbase-core/src/js/runtime.rs b/trailbase-core/src/js/runtime.rs index 41ed9d22..5f46d6ab 100644 --- a/trailbase-core/src/js/runtime.rs +++ b/trailbase-core/src/js/runtime.rs @@ -758,21 +758,20 @@ pub(crate) async fn load_routes_from_js_modules( } }; - let mut js_router = Some(Router::new()); + let mut js_router = Router::new(); for module in modules { let fname = module.filename().to_owned(); let router = install_routes(state.script_runtime(), module).await?; if let Some(router) = router { - js_router = Some(js_router.take().unwrap().nest("/", router)); + js_router = js_router.merge(router); } else { log::debug!("Skipping js module '{fname:?}': no routes"); } } - let router = js_router.take().unwrap(); - if router.has_routes() { - return Ok(Some(router)); + if js_router.has_routes() { + return Ok(Some(js_router)); } return Ok(None); diff --git a/trailbase-core/src/records/mod.rs b/trailbase-core/src/records/mod.rs index 558fdbfe..3e92aeec 100644 --- a/trailbase-core/src/records/mod.rs +++ b/trailbase-core/src/records/mod.rs @@ -24,6 +24,7 @@ pub(crate) use validate::validate_record_api_config; use crate::config::proto::{PermissionFlag, RecordApiConfig}; use crate::config::ConfigError; +use crate::constants::RECORD_API_PATH; use crate::AppState; #[derive(OpenApi)] @@ -44,26 +45,38 @@ pub(super) struct RecordOpenApi; pub(crate) fn router() -> Router { return Router::new() - .route("/:name/:record", get(read_record::read_record_handler)) - .route("/:name", post(create_record::create_record_handler)) .route( - "/:name/:record", + &format!("/{RECORD_API_PATH}/{{name}}/{{record}}"), + get(read_record::read_record_handler), + ) + .route( + &format!("/{RECORD_API_PATH}/{{name}}"), + post(create_record::create_record_handler), + ) + .route( + &format!("/{RECORD_API_PATH}/{{name}}/{{record}}"), patch(update_record::update_record_handler), ) .route( - "/:name/:record", + &format!("/{RECORD_API_PATH}/{{name}}/{{record}}"), delete(delete_record::delete_record_handler), ) - .route("/:name", get(list_records::list_records_handler)) .route( - "/:name/:record/file/:column_name", + &format!("/{RECORD_API_PATH}/{{name}}"), + get(list_records::list_records_handler), + ) + .route( + &format!("/{RECORD_API_PATH}/{{name}}/{{record}}/file/{{column_name}}"), get(read_record::get_uploaded_file_from_record_handler), ) .route( - "/:name/:record/files/:column_name/:file_index", + &format!("/{RECORD_API_PATH}/{{name}}/{{record}}/files/{{column_name}}/{{file_index}}"), get(read_record::get_uploaded_files_from_record_handler), ) - .route("/:name/schema", get(json_schema::json_schema_handler)); + .route( + &format!("/{RECORD_API_PATH}/{{name}}/schema"), + get(json_schema::json_schema_handler), + ); } // Since this is for APIs access control, we'll use the API- space CRUD terminology instead of diff --git a/trailbase-core/src/server/mod.rs b/trailbase-core/src/server/mod.rs index 4c86c5b6..757ee0a7 100644 --- a/trailbase-core/src/server/mod.rs +++ b/trailbase-core/src/server/mod.rs @@ -19,9 +19,10 @@ use crate::app_state::AppState; use crate::assets::AssetService; use crate::auth::util::is_admin; use crate::auth::{self, AuthError, User}; -use crate::constants::{ADMIN_API_PATH, AUTH_API_PATH, HEADER_CSRF_TOKEN, RECORD_API_PATH}; +use crate::constants::{ADMIN_API_PATH, HEADER_CSRF_TOKEN}; use crate::data_dir::DataDir; use crate::logging; +use crate::records; use crate::scheduler; pub use init::{init_app_state, InitArgs, InitError}; @@ -114,7 +115,7 @@ impl Server { .map_err(|err| InitError::ScriptError(err.to_string()))?; if let Some(js_routes) = js_routes { - Some(custom_routes.unwrap_or_default().nest("/", js_routes)) + Some(custom_routes.unwrap_or_default().merge(js_routes)) } else { custom_routes } @@ -233,8 +234,8 @@ impl Server { } let router = Router::new() - .nest(&format!("/{AUTH_API_PATH}"), auth::admin_auth_router()) - .nest("/", Self::build_admin_router(state)); + .merge(auth::admin_auth_router()) + .merge(Self::build_admin_router(state)); return Some(( address.clone(), @@ -249,20 +250,20 @@ impl Server { ) -> (String, Router<()>) { let mut router = Router::new() // Public, stable and versioned APIs. - .nest(&format!("/{RECORD_API_PATH}"), crate::records::router()) - .nest(&format!("/{AUTH_API_PATH}"), auth::router()) + .merge(records::router()) + .merge(auth::router()) .route("/api/healthcheck", get(healthcheck_handler)); if !has_indepenedent_admin_router(opts) { - router = router.nest("/", Self::build_admin_router(state)); + router = router.merge(Self::build_admin_router(state)); } if !opts.disable_auth_ui { - router = router.nest("/_/auth", crate::auth::auth_ui_router()); + router = router.merge(auth::auth_ui_router()); } if let Some(custom_router) = custom_router { - router = router.nest("/", custom_router); + router = router.merge(custom_router); } if let Some(public_dir) = &opts.public_dir {