Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
maxcountryman committed Sep 20, 2023
1 parent 523c466 commit 28b49ec
Show file tree
Hide file tree
Showing 15 changed files with 3,820 additions and 12 deletions.
2,032 changes: 2,032 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

48 changes: 46 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,54 @@
[package]
name = "tower-sessions"
description = "Session manager middleware for tower."
description = "🥠 Cookie-based sessions as a `tower` middleware."
version = "0.0.0"
edition = "2021"
license = "MIT"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["axum-core", "memory-store"]
memory-store = []
redis-store = ["fred"]
sqlite-store = ["sqlx", "sqlx/sqlite"]

[dependencies]
async-trait = "0.1"
http = "0.2"
parking_lot = { version = "0.12", features = ["nightly", "serde"] }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
thiserror = "1.0.48"
time = { version = "0.3", features = ["serde"] }
tower-cookies = "0.9"
tower-layer = "0.3"
tower-service = "0.3"
uuid = { version = "1.4.1", features = ["v4", "serde"] }
axum-core = { optional = true, version = "0.3" }
fred = { optional = true, version = "6", features = ["serde-json"] }
sqlx = { optional = true, version = "0.7.1", features = [
"time",
"uuid",
"runtime-tokio",
] }

[dev-dependencies]
axum = "0.6"
hyper = "0.14"
tokio = { version = "1", features = ["full"] }
tower = "0.4"

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[[example]]
name = "counter"
required-features = ["axum-core", "memory-store"]

[[example]]
name = "redis-store"
required-features = ["axum-core", "redis-store"]

[[example]]
name = "sqlite-store"
required-features = ["axum-core", "sqlite-store"]
102 changes: 102 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<h1 align="center">
tower-sessions
</h1>

<p align="center">
🥠 Cookie-based sessions as a `tower` middleware.
</p>

<div align="center">
<a href="https://crates.io/crates/tower-sessions">
<img src="https://img.shields.io/crates/v/tower-sessions.svg" />
</a>
<a href="https://docs.rs/tower-sessions">
<img src="https://docs.rs/tower-sessions/badge.svg" />
</a>
<a href="https://github.com/maxcountryman/tower-sessions/actions/workflows/rust.yml">
<img src="https://github.com/maxcountryman/tower-sessions/actions/workflows/rust.yml/badge.svg" />
</a>
</div>

## 🎨 Overview

This crate provides cookie-based sessions as a `tower` middleware.

- Wraps `tower-cookies` for cookie management
- Decouples sessions from their storage (`SessionStore`)
- `Session` works as an extractor when using `axum`
- Redis and SQLx stores provided via feature flags
- Works directly with types that implement `Serialize` and `Deserialize`

## 📦 Install

To use the crate in your project, add the following to your `Cargo.toml` file:

```toml
[dependencies]
tower-sessions = "0.0.0"
```

## 🤸 Usage

### `axum` Example

```rust
use std::net::SocketAddr;

use axum::{
error_handling::HandleErrorLayer, response::IntoResponse, routing::get, BoxError, Router,
};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use tower::ServiceBuilder;
use tower_sessions::{time::Duration, MemoryStore, Session, SessionManagerLayer};

#[derive(Default, Deserialize, Serialize)]
struct Counter(usize);

#[tokio::main]
async fn main() {
let session_store = MemoryStore::default();
let session_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(
SessionManagerLayer::new(session_store)
.with_secure(false)
.with_max_age(Duration::seconds(10)),
);

let app = Router::new()
.route("/", get(handler))
.layer(session_service);

let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

async fn handler(session: Session) -> impl IntoResponse {
let counter: Counter = session
.get("counter")
.expect("Could not deserialize.")
.unwrap_or_default();

session
.insert("counter", counter.0 + 1)
.expect("Could not serialize.");

format!("Current count: {}", counter.0)
}
```

You can find this [example][counter-example] as well as other example projects in the [example directory][examples].

See the [crate documentation][docs] for more usage information.

[counter-example]: https://github.com/maxcountryman/tower-sessions/tree/main/examples/counter.rs
[examples]: https://github.com/maxcountryman/tower-sessions/tree/main/examples
[docs]: https://docs.rs/tower-sessions
49 changes: 49 additions & 0 deletions examples/counter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use std::net::SocketAddr;

use axum::{
error_handling::HandleErrorLayer, response::IntoResponse, routing::get, BoxError, Router,
};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use tower::ServiceBuilder;
use tower_sessions::{time::Duration, MemoryStore, Session, SessionManagerLayer};

#[derive(Default, Deserialize, Serialize)]
struct Counter(usize);

#[tokio::main]
async fn main() {
let session_store = MemoryStore::default();
let session_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(
SessionManagerLayer::new(session_store)
.with_secure(false)
.with_max_age(Duration::seconds(10)),
);

let app = Router::new()
.route("/", get(handler))
.layer(session_service);

let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

async fn handler(session: Session) -> impl IntoResponse {
let counter: Counter = session
.get("counter")
.expect("Could not deserialize.")
.unwrap_or_default();

session
.insert("counter", counter.0 + 1)
.expect("Could not serialize.");

format!("Current count: {}", counter.0)
}
55 changes: 55 additions & 0 deletions examples/redis-store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use std::net::SocketAddr;

use axum::{
error_handling::HandleErrorLayer, response::IntoResponse, routing::get, BoxError, Router,
};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use tower::ServiceBuilder;
use tower_sessions::{fred::prelude::*, time::Duration, RedisStore, Session, SessionManagerLayer};

#[derive(Serialize, Deserialize, Default)]
struct Counter(usize);

#[tokio::main]
async fn main() {
let config = RedisConfig::from_url("redis://127.0.0.1:6379/1").unwrap();
let client = RedisClient::new(config, None, None);

let _ = client.connect();
let _ = client.wait_for_connect().await.unwrap();

let session_store = RedisStore::new(client);
let session_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(
SessionManagerLayer::new(session_store)
.with_secure(false)
.with_max_age(Duration::seconds(10)),
);

let app = Router::new()
.route("/", get(handler))
.layer(session_service);

let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

async fn handler(session: Session) -> impl IntoResponse {
let counter: Counter = session
.get("counter")
.expect("Could not deserialize.")
.unwrap_or_default();

session
.insert("counter", counter.0 + 1)
.expect("Could not serialize.");

format!("Current count: {}", counter.0)
}
51 changes: 51 additions & 0 deletions examples/sqlite-store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use std::net::SocketAddr;

use axum::{
error_handling::HandleErrorLayer, response::IntoResponse, routing::get, BoxError, Router,
};
use http::StatusCode;
use serde::{Deserialize, Serialize};
use tower::ServiceBuilder;
use tower_sessions::{sqlx::SqlitePool, time::Duration, Session, SessionManagerLayer, SqliteStore};

#[derive(Serialize, Deserialize, Default)]
struct Counter(usize);

#[tokio::main]
async fn main() {
let pool = SqlitePool::connect("sqlite::memory:").await.unwrap();
let session_store = SqliteStore::new(pool);
session_store.migrate().await.unwrap();
let session_service = ServiceBuilder::new()
.layer(HandleErrorLayer::new(|_: BoxError| async {
StatusCode::BAD_REQUEST
}))
.layer(
SessionManagerLayer::new(session_store)
.with_secure(false)
.with_max_age(Duration::seconds(10)),
);

let app = Router::new()
.route("/", get(handler))
.layer(session_service);

let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}

async fn handler(session: Session) -> impl IntoResponse {
let counter: Counter = session
.get("counter")
.expect("Could not deserialize.")
.unwrap_or_default();

session
.insert("counter", counter.0 + 1)
.expect("Could not serialize.");

format!("Current count: {}", counter.0)
}
5 changes: 5 additions & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
format_code_in_doc_comments = true
format_strings = true
imports_granularity = "Crate"
group_imports = "StdExternalCrate"
wrap_comments = true
20 changes: 20 additions & 0 deletions src/extract.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use async_trait::async_trait;
use axum_core::extract::FromRequestParts;
use http::{request::Parts, StatusCode};

use crate::session::Session;

#[async_trait]
impl<S> FromRequestParts<S> for Session
where
S: Sync + Send,
{
type Rejection = (http::StatusCode, &'static str);

async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
parts.extensions.get::<Session>().cloned().ok_or((
StatusCode::INTERNAL_SERVER_ERROR,
"Can't extract session. Is `SessionManagerLayer` enabled?",
))
}
}
Loading

0 comments on commit 28b49ec

Please sign in to comment.