Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 3 additions & 9 deletions cot-cli/src/project_template/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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()
}
}
Expand Down
59 changes: 59 additions & 0 deletions cot/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -1602,7 +1645,10 @@ impl From<Expiry> 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.
///
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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"
Expand All @@ -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!(
Expand All @@ -2565,13 +2620,16 @@ 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,
StaticFilesPathRewriteMode::None
);
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);
Expand All @@ -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);
}

Expand Down
156 changes: 152 additions & 4 deletions cot/src/middleware.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -102,6 +103,83 @@ pub use live_reload::LiveReloadMiddleware;
pub use trailing_slash::{TrailingSlashMiddleware, TrailingSlashService};

type DynamicSessionStore = SessionManagerLayer<SessionStoreWrapper, PlaintextCookie>;
type OptionalLayer<L> = tower::util::Either<L, tower::layer::util::Identity>;
#[cfg(feature = "live-reload")]
type ConfiguredLiveReloadMiddleware = LiveReloadMiddleware;
#[cfg(not(feature = "live-reload"))]
type ConfiguredLiveReloadMiddleware = tower::layer::util::Identity;
type CommonMiddlewareLayer = (

@ElijahAhianyo ElijahAhianyo Aug 9, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not exactly sure if this is the right approach. I would expect the CommonMiddleware to be a stack of default middlewares that are enabled as a group by default without users manually registering them at startup time, not particularly one where some can be enabled and some disabled. That defeats the purpose of grouping them. If users want all default middlewares enabled, they should only have to register this middleware; however, they should be able to opt out and manually register ones they want.

A rough idea I had in mind was along the lines of:

use tower::Layer;

use crate::middleware::TrailingSlashMiddleware;
use crate::project::MiddlewareContext;

#[derive(Debug, Clone)]
pub struct CommonMiddleware {
    trailing_slash: TrailingSlashMiddleware,
}

impl CommonMiddleware {
    #[must_use]
    pub fn from_context(context: &MiddlewareContext) -> Self {
        Self {
            trailing_slash: TrailingSlashMiddleware::from_context(context),
        }
    }
}

impl<S> Layer<S> for CommonMiddleware {
    type Service = <TrailingSlashMiddleware as Layer<S>>::Service;

    fn layer(&self, inner: S) -> Self::Service {
        self.trailing_slash.layer(inner)
    }
}

If we decide to add the session and auth middlewares(as an example) to the common middlewares, then it will look something like this:

#[derive(Debug, Clone)]
pub struct CommonMiddleware {
    trailing_slash: TrailingSlashMiddleware,
    session: SessionMiddleware,
    auth: AuthMiddleware,
}

impl CommonMiddleware {
    #[must_use]
    pub fn from_context(context: &MiddlewareContext) -> Self {
        Self {
            trailing_slash: TrailingSlashMiddleware::from_context(context),
            session: SessionMiddleware::from_context(context),
            auth: AuthMiddleware::new(),
        }
    }
}

impl<S> Layer<S> for CommonMiddleware
where
    AuthMiddleware: Layer<S>,
    SessionMiddleware: Layer<<AuthMiddleware as Layer<S>>::Service>,
    TrailingSlashMiddleware:
    Layer<<SessionMiddleware as Layer<<AuthMiddleware as Layer<S>>::Service>>::Service>,
{
    type Service = <TrailingSlashMiddleware as Layer<
        <SessionMiddleware as Layer<<AuthMiddleware as Layer<S>>::Service>>::Service,
    >>::Service;

    fn layer(&self, inner: S) -> Self::Service {
        let service = self.auth.layer(inner);
        let service = self.session.layer(service);
        self.trailing_slash.layer(service)
    }
}

What I am not sure of is if we want to have the auth, session, and live middlewares as part of the common middlewares since they may not necessarily be the common denominator of every app. @m4tx Do you want to weigh in on which middlewares should be labelled as common?

One other thing to note, which I believe should be a separate issue(and not within the scope of this PR), is that registering the common middleware along with middlewares already present in the common middleware (or duplicating middlewares in general) could be wasteful.

handler
    .middleware(CommonMiddleware::from_context(context))
    .middleware(TrailingSlashMiddleware::from_context(context))

I believe this should be permitted; however, we should probably warn the user of this at runtime

OptionalLayer<TrailingSlashMiddleware>,
OptionalLayer<ConfiguredLiveReloadMiddleware>,
OptionalLayer<SessionMiddleware>,
OptionalLayer<AuthMiddleware>,
OptionalLayer<StaticFilesMiddleware>,
);

/// 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::<tower::layer::util::Identity>);

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<S> tower::Layer<S> for CommonMiddleware {
type Service = <CommonMiddlewareLayer as tower::Layer<S>>::Service;

fn layer(&self, inner: S) -> Self::Service {
self.0.layer(inner)
}
}

/// A middleware that provides session management.
///
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -752,6 +831,75 @@ mod tests {
fn register_apps(&self, _apps: &mut AppBuilder, _context: &RegisterAppsContext) {}
}

struct CommonMiddlewareApp;

async fn common_middleware_page(_request: Request<Body>) -> crate::Result<Response> {
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<ProjectConfig> {
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);
Expand Down
7 changes: 3 additions & 4 deletions docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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;
Expand Down
Loading