From 6e4715bae9d0f77865008688090dbc40f1b5068a Mon Sep 17 00:00:00 2001 From: ZhangStudyLife <174326754+ZhangStudyLife@users.noreply.github.com> Date: Sun, 9 Aug 2026 07:59:51 +0800 Subject: [PATCH] feat: add common middleware --- cot-cli/src/project_template/src/main.rs | 12 +- cot/src/config.rs | 59 +++++++++ cot/src/middleware.rs | 156 ++++++++++++++++++++++- docs/introduction.md | 7 +- 4 files changed, 217 insertions(+), 17 deletions(-) diff --git a/cot-cli/src/project_template/src/main.rs b/cot-cli/src/project_template/src/main.rs index d22127abc..9b5313a27 100644 --- a/cot-cli/src/project_template/src/main.rs +++ b/cot-cli/src/project_template/src/main.rs @@ -4,13 +4,11 @@ use cot::auth::db::DatabaseUserApp; use cot::cli::CliMetadata; use cot::db::migrations::SyncDynMigration; use cot::html::Html; -use cot::middleware::{ - AuthMiddleware, LiveReloadMiddleware, SessionMiddleware, TrailingSlashMiddleware, -}; +use cot::middleware::CommonMiddleware; use cot::project::{MiddlewareContext, RegisterAppsContext, RootHandler, RootHandlerBuilder}; use cot::request::extractors::StaticFiles; use cot::router::{Route, Router}; -use cot::static_files::{StaticFile, StaticFilesMiddleware}; +use cot::static_files::StaticFile; use cot::session::db::SessionApp; use cot::{App, AppBuilder, Project, static_files, Template}; @@ -66,11 +64,7 @@ impl Project for {{ project_struct_name }} { context: &MiddlewareContext, ) -> RootHandler { handler - .middleware(StaticFilesMiddleware::from_context(context)) - .middleware(AuthMiddleware::new()) - .middleware(SessionMiddleware::from_context(context)) - .middleware(LiveReloadMiddleware::from_context(context)) - .middleware(TrailingSlashMiddleware::from_context(context)) + .middleware(CommonMiddleware::from_context(context)) .build() } } diff --git a/cot/src/config.rs b/cot/src/config.rs index 24260d124..7d913a122 100644 --- a/cot/src/config.rs +++ b/cot/src/config.rs @@ -1197,10 +1197,16 @@ impl StaticFilesConfig { #[serde(default)] #[non_exhaustive] pub struct MiddlewareConfig { + /// The configuration for the authentication middleware. + pub auth: EnabledMiddlewareConfig, + /// The configuration for the static files middleware. + pub static_files: EnabledMiddlewareConfig, /// The configuration for the live reload middleware. pub live_reload: LiveReloadMiddlewareConfig, /// The configuration for the session middleware. pub session: SessionMiddlewareConfig, + /// The configuration for the trailing slash middleware. + pub trailing_slash: EnabledMiddlewareConfig, } impl MiddlewareConfig { @@ -1236,12 +1242,49 @@ impl MiddlewareConfigBuilder { #[must_use] pub fn build(&self) -> MiddlewareConfig { MiddlewareConfig { + auth: self.auth.clone().unwrap_or_default(), + static_files: self.static_files.clone().unwrap_or_default(), live_reload: self.live_reload.clone().unwrap_or_default(), session: self.session.clone().unwrap_or_default(), + trailing_slash: self.trailing_slash.clone().unwrap_or_default(), } } } +/// Configuration shared by middleware that can simply be enabled or disabled. +#[derive(Debug, Clone, PartialEq, Eq, Builder, Serialize, Deserialize)] +#[builder(build_fn(skip, error = std::convert::Infallible))] +#[serde(default)] +#[non_exhaustive] +pub struct EnabledMiddlewareConfig { + /// Whether the middleware is enabled. + pub enabled: bool, +} + +impl EnabledMiddlewareConfig { + /// Create a new [`EnabledMiddlewareConfigBuilder`]. + #[must_use] + pub fn builder() -> EnabledMiddlewareConfigBuilder { + EnabledMiddlewareConfigBuilder::default() + } +} + +impl EnabledMiddlewareConfigBuilder { + /// Builds the middleware configuration. + #[must_use] + pub fn build(&self) -> EnabledMiddlewareConfig { + EnabledMiddlewareConfig { + enabled: self.enabled.unwrap_or(true), + } + } +} + +impl Default for EnabledMiddlewareConfig { + fn default() -> Self { + Self::builder().build() + } +} + /// The configuration for the live reload middleware. /// /// This is used as part of the [`MiddlewareConfig`] struct. @@ -1602,7 +1645,10 @@ impl From for tower_sessions::Expiry { #[builder(build_fn(skip, error = std::convert::Infallible))] #[serde(default)] #[non_exhaustive] +#[expect(clippy::struct_excessive_bools)] pub struct SessionMiddlewareConfig { + /// Whether the session middleware is enabled. + pub enabled: bool, /// The [`Secure`] of the cookie determines whether the session middleware /// is secure. /// @@ -1838,6 +1884,7 @@ impl SessionMiddlewareConfigBuilder { #[must_use] pub fn build(&self) -> SessionMiddlewareConfig { SessionMiddlewareConfig { + enabled: self.enabled.unwrap_or(true), secure: self.secure.unwrap_or(true), http_only: self.http_only.unwrap_or(true), same_site: self.same_site.unwrap_or_default(), @@ -2515,8 +2562,12 @@ mod tests { cache_timeout = "1h" [middlewares] + auth.enabled = false + static_files.enabled = false live_reload.enabled = true + trailing_slash.enabled = false [middlewares.session] + enabled = false secure = false http_only = false domain = "localhost" @@ -2543,7 +2594,11 @@ mod tests { config.static_files.cache_timeout, Some(Duration::from_hours(1)) ); + assert!(!config.middlewares.auth.enabled); + assert!(!config.middlewares.static_files.enabled); assert!(config.middlewares.live_reload.enabled); + assert!(!config.middlewares.session.enabled); + assert!(!config.middlewares.trailing_slash.enabled); assert!(!config.middlewares.session.secure); assert!(!config.middlewares.session.http_only); assert_eq!( @@ -2565,6 +2620,8 @@ mod tests { assert_eq!(config.secret_key.as_bytes(), b""); assert_eq!(config.fallback_secret_keys.len(), 0); assert_eq!(config.auth_backend, AuthBackendConfig::None); + assert!(config.middlewares.auth.enabled); + assert!(config.middlewares.static_files.enabled); assert_eq!(config.static_files.url, "/static/"); assert_eq!( config.static_files.rewrite, @@ -2572,6 +2629,7 @@ mod tests { ); assert_eq!(config.static_files.cache_timeout, None); assert!(!config.middlewares.live_reload.enabled); + assert!(config.middlewares.session.enabled); assert!(config.middlewares.session.secure); assert!(config.middlewares.session.http_only); assert_eq!(config.middlewares.session.domain, None); @@ -2584,6 +2642,7 @@ mod tests { config.middlewares.session.store.store_type, SessionStoreTypeConfig::Memory ); + assert!(config.middlewares.trailing_slash.enabled); assert_eq!(config.database.url, None); } diff --git a/cot/src/middleware.rs b/cot/src/middleware.rs index 62a54d684..881b2d9ce 100644 --- a/cot/src/middleware.rs +++ b/cot/src/middleware.rs @@ -29,6 +29,7 @@ use crate::session::store::file::FileStore; use crate::session::store::memory::MemoryStore; #[cfg(feature = "redis")] use crate::session::store::redis::RedisStore; +use crate::static_files::StaticFilesMiddleware; #[cfg(feature = "live-reload")] mod live_reload; @@ -102,6 +103,83 @@ pub use live_reload::LiveReloadMiddleware; pub use trailing_slash::{TrailingSlashMiddleware, TrailingSlashService}; type DynamicSessionStore = SessionManagerLayer; +type OptionalLayer = tower::util::Either; +#[cfg(feature = "live-reload")] +type ConfiguredLiveReloadMiddleware = LiveReloadMiddleware; +#[cfg(not(feature = "live-reload"))] +type ConfiguredLiveReloadMiddleware = tower::layer::util::Identity; +type CommonMiddlewareLayer = ( + OptionalLayer, + OptionalLayer, + OptionalLayer, + OptionalLayer, + OptionalLayer, +); + +/// A collection of the middlewares enabled by default in generated projects. +/// +/// Each child middleware can be disabled in the project configuration under +/// `[middlewares]`. The order is the same as the generated project template. +/// +/// ```toml +/// [middlewares] +/// auth.enabled = false +/// static_files.enabled = false +/// live_reload.enabled = false +/// session.enabled = false +/// trailing_slash.enabled = false +/// ``` +#[derive(Debug, Clone)] +pub struct CommonMiddleware(CommonMiddlewareLayer); + +impl CommonMiddleware { + /// Creates common middleware from the project context and its + /// configuration. + #[must_use] + pub fn from_context(context: &MiddlewareContext) -> Self { + let config = &context.config().middlewares; + #[cfg(feature = "live-reload")] + let live_reload = tower::util::option_layer( + config + .live_reload + .enabled + .then(|| LiveReloadMiddleware::from_context(context)), + ); + #[cfg(not(feature = "live-reload"))] + let live_reload = tower::util::option_layer(None::); + + Self(( + tower::util::option_layer( + config + .trailing_slash + .enabled + .then(|| TrailingSlashMiddleware::from_context(context)), + ), + live_reload, + tower::util::option_layer( + config + .session + .enabled + .then(|| SessionMiddleware::from_context(context)), + ), + tower::util::option_layer(config.auth.enabled.then(AuthMiddleware::new)), + tower::util::option_layer( + config + .static_files + .enabled + .then(|| StaticFilesMiddleware::from_context(context)), + ), + )) + } +} + +impl tower::Layer for CommonMiddleware { + type Service = >::Service; + + fn layer(&self, inner: S) -> Self::Service { + self.0.layer(inner) + } +} /// A middleware that provides session management. /// @@ -557,21 +635,22 @@ mod tests { use std::path::PathBuf; use std::sync::Arc; - use http::Request; + use http::{Request, StatusCode}; use tower::{Layer, Service, ServiceExt}; use super::*; use crate::auth::Auth; use crate::config::{ - CacheUrl, DatabaseConfig, MiddlewareConfig, ProjectConfig, SessionMiddlewareConfig, - SessionStoreConfig, SessionStoreTypeConfig, + CacheUrl, DatabaseConfig, EnabledMiddlewareConfig, MiddlewareConfig, ProjectConfig, + SessionMiddlewareConfig, SessionStoreConfig, SessionStoreTypeConfig, }; use crate::middleware::SessionMiddleware; use crate::project::{RegisterAppsContext, WithCache}; use crate::response::Response; + use crate::router::{Route, Router}; use crate::session::Session; use crate::test::TestRequestBuilder; - use crate::{AppBuilder, Body, Bootstrapper, Error, Project, ProjectContext}; + use crate::{App, AppBuilder, Body, Bootstrapper, Error, Project, ProjectContext}; #[cot::test] async fn session_middleware_adds_session() { @@ -752,6 +831,75 @@ mod tests { fn register_apps(&self, _apps: &mut AppBuilder, _context: &RegisterAppsContext) {} } + struct CommonMiddlewareApp; + + async fn common_middleware_page(_request: Request) -> crate::Result { + Ok(Response::new(Body::empty())) + } + + impl App for CommonMiddlewareApp { + fn name(&self) -> &'static str { + "common_middleware_app" + } + + fn router(&self) -> Router { + Router::with_urls([Route::with_handler("/page/", common_middleware_page)]) + } + } + + struct CommonMiddlewareProject { + trailing_slash_enabled: bool, + } + + impl Project for CommonMiddlewareProject { + fn config(&self, _config_name: &str) -> crate::Result { + Ok(ProjectConfig::builder() + .middlewares( + MiddlewareConfig::builder() + .auth(EnabledMiddlewareConfig::builder().enabled(false).build()) + .session(SessionMiddlewareConfig::builder().enabled(false).build()) + .trailing_slash( + EnabledMiddlewareConfig::builder() + .enabled(self.trailing_slash_enabled) + .build(), + ) + .build(), + ) + .build()) + } + + fn register_apps(&self, apps: &mut AppBuilder, _context: &RegisterAppsContext) { + apps.register_with_views(CommonMiddlewareApp, ""); + } + + fn middlewares( + &self, + handler: crate::project::RootHandlerBuilder, + context: &MiddlewareContext, + ) -> crate::project::RootHandler { + handler + .middleware(CommonMiddleware::from_context(context)) + .build() + } + } + + #[cot::test] + async fn common_middleware_respects_trailing_slash_config() { + let mut enabled_client = crate::test::Client::new(CommonMiddlewareProject { + trailing_slash_enabled: true, + }) + .await; + let response = enabled_client.get("/page").await.unwrap(); + assert_eq!(response.status(), StatusCode::PERMANENT_REDIRECT); + + let mut disabled_client = crate::test::Client::new(CommonMiddlewareProject { + trailing_slash_enabled: false, + }) + .await; + let response = disabled_client.get("/page").await.unwrap(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); + } + #[cot::test] async fn memory_store_factory_produces_working_store() { let config = create_project_config(SessionStoreTypeConfig::Memory); diff --git a/docs/introduction.md b/docs/introduction.md index c28bd48c4..fc9284bab 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -255,7 +255,7 @@ This defines the project and sets the CLI metadata (like the name, version, and This registers all the apps that your project is using. ```rust -# use cot::middleware::LiveReloadMiddleware; +# use cot::middleware::CommonMiddleware; # struct CotTutorialProject; # impl Project for CotTutorialProject { fn middlewares( @@ -264,14 +264,13 @@ This registers all the apps that your project is using. context: &MiddlewareContext, ) -> RootHandler { handler - .middleware(StaticFilesMiddleware::from_context(context)) - .middleware(LiveReloadMiddleware::from_context(context)) + .middleware(CommonMiddleware::from_context(context)) .build() } # } ``` -This registers the middlewares that will be applied to all routes in the project. Note that the [`LiveReloadMiddleware`](struct@cot::middleware::LiveReloadMiddleware) may be dynamically disabled in runtime using config! +This registers the default middlewares that will be applied to all routes in the project. Each child middleware in [`CommonMiddleware`](struct@cot::middleware::CommonMiddleware) may be disabled in runtime using config. ```rust,has_main # use cot::Project;