Skip to content
Merged
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
87 changes: 82 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
bon = "3.7.2"
fred = { version = "10.1", optional = true, default-features = false, features = [
"i-keys",
"i-hashes",
Expand Down
70 changes: 21 additions & 49 deletions src/fairing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::{
sync::{Arc, Mutex},
};

use bon::Builder;
use rocket::{fairing::Fairing, Build, Orbit, Request, Response, Rocket};

use crate::{
Expand Down Expand Up @@ -52,24 +53,21 @@ fn rocket() -> _ {
}
```
*/
#[derive(Clone)]
pub struct RocketFlexSession<T> {
#[derive(Builder)]
pub struct RocketFlexSession<T: Send + Sync + Clone + 'static> {
/// Set the options directly. Alternatively, use `with_options` to customize the default options via a closure.
#[builder(default)]
pub(crate) options: RocketFlexSessionOptions,
#[builder(default = Arc::new(MemoryStorage::default()), with = |storage: impl SessionStorage<T> + 'static| Arc::new(storage))]
/// Set the session storage provider. The default is an in-memory storage.
pub(crate) storage: Arc<dyn SessionStorage<T>>,
}
impl<T> RocketFlexSession<T>
where
T: Send + Sync + Clone + 'static,
{
/// Build a session configuration
pub fn builder() -> RocketFlexSessionBuilder<T> {
RocketFlexSessionBuilder::default()
}
}

impl<T> Default for RocketFlexSession<T>
where
T: Send + Sync + Clone + 'static,
{
/// Create a new instance with default options and an in-memory storage.
fn default() -> Self {
Self {
options: Default::default(),
Expand All @@ -78,50 +76,24 @@ where
}
}

/// Builder to configure the [RocketFlexSession] fairing
pub struct RocketFlexSessionBuilder<T>
where
T: Send + Sync + Clone + 'static,
{
fairing: RocketFlexSession<T>,
}
impl<T> Default for RocketFlexSessionBuilder<T>
where
T: Send + Sync + Clone + 'static,
{
fn default() -> Self {
Self {
fairing: Default::default(),
}
}
}
impl<T> RocketFlexSessionBuilder<T>
use rocket_flex_session_builder::{IsUnset, SetOptions, State};
impl<T, S> RocketFlexSessionBuilder<T, S>
where
T: Send + Sync + Clone + 'static,
S: State,
{
/// Set the session options via a closure. If you're using a cookie-based storage
/// provider, make sure to set the corresponding cookie settings
/// in the storage configuration as well.
pub fn with_options<OptionsFn>(&mut self, options_fn: OptionsFn) -> &mut Self
/// Customize the [options](RocketFlexSessionOptions) via a closure. Any options that are not set will retain their default values.
pub fn with_options<OptionsFn>(
self,
options_fn: OptionsFn,
) -> RocketFlexSessionBuilder<T, SetOptions<S>>
where
S::Options: IsUnset,
OptionsFn: FnOnce(&mut RocketFlexSessionOptions),
{
options_fn(&mut self.fairing.options);
self
}

/// Set the session storage provider
pub fn storage<S>(&mut self, storage: S) -> &mut Self
where
S: SessionStorage<T> + 'static,
{
self.fairing.storage = Arc::new(storage);
self
}

/// Build the fairing
pub fn build(&self) -> RocketFlexSession<T> {
self.fairing.clone()
let mut options = RocketFlexSessionOptions::default();
options_fn(&mut options);
self.options(options)
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,7 @@ mod session_inner;

pub mod error;
pub mod storage;
pub use fairing::{RocketFlexSession, RocketFlexSessionBuilder};
pub use fairing::RocketFlexSession;
pub use options::RocketFlexSessionOptions;
pub use session::Session;
pub use session_hash::SessionHashMap;
Expand Down
38 changes: 15 additions & 23 deletions src/storage/redis/base.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use bon::Builder;
use fred::{
prelude::{HashesInterface, KeysInterface, Pool, Value},
prelude::{HashesInterface, KeysInterface, Value},
types::Expiration,
};

Expand All @@ -15,15 +16,14 @@ pub enum RedisType {
/**
Redis session storage using the [fred.rs](https://docs.rs/fred) crate.

# Requirements
You can store the data as a Redis string or hash. Your session data type must implement [`FromValue`](https://docs.rs/fred/latest/fred/types/trait.FromValue.html)
from the fred.rs crate, as well as the inverse `From<MyData>` or `TryFrom<MyData>` for [`Value`](https://docs.rs/fred/latest/fred/types/enum.Value.html) in order
to dictate how the data will be converted to/from the Redis data type.
- For Redis string types, convert to/from `Value::String`
- For Redis hash types, convert to/from `Value::Map`

💡 Common hashmap types like `HashMap<String, String>` are automatically supported - make sure to use `RedisType::Hash`
when constructing the storage to ensure they are properly converted and stored as Redis hashes.

# Setup
```rust
use fred::prelude::{Builder, ClientLike, Config, FromValue, Value};
use rocket_flex_session::{error::SessionError, storage::{redis::{RedisFredStorage, RedisType}}};
Expand All @@ -37,16 +37,16 @@ async fn setup_storage() -> RedisFredStorage {
redis_pool.init().await.expect("Should initialize Redis pool");

// Construct the storage
let storage = RedisFredStorage::new(
redis_pool,
RedisType::String, // or RedisType::Hash
"sess:" // Prefix for Redis keys
);
let storage = RedisFredStorage::builder()
.pool(redis_pool)
.prefix("sess:") // Prefix for Redis keys
.redis_type(RedisType::String) // or RedisType::Hash
.build();

storage
}

// If using a custom struct for your session data, implement the following...
// Implement the following for your session data type...
struct MySessionData {
user_id: String,
}
Expand All @@ -67,26 +67,18 @@ impl From<MySessionData> for Value {
}
```
*/
#[derive(Builder)]
pub struct RedisFredStorage {
/// The initialized fred.rs connection pool.
pub(super) pool: fred::prelude::Pool,
/// The prefix to use for session keys.
#[builder(into, default = "sess:")]
pub(super) prefix: String,
/// The Redis data type to use for storing sessions.
pub(super) redis_type: RedisType,
}

impl RedisFredStorage {
/// Create the storage instance.
/// # Parameters
/// * `pool` - The initialized fred.rs connection pool.
/// * `redis_type` - The Redis data type to use for storing sessions.
/// * `key_prefix` - The prefix to use for session keys. (e.g. "sess:")
pub fn new(pool: Pool, redis_type: RedisType, key_prefix: &str) -> Self {
Self {
pool,
prefix: key_prefix.to_owned(),
redis_type,
}
}

pub(super) fn session_key(&self, id: &str) -> String {
format!("{}{id}", self.prefix)
}
Expand Down
26 changes: 11 additions & 15 deletions src/storage/redis/storage_indexed.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use bon::Builder;
use fred::prelude::{FromValue, HashesInterface, KeysInterface, SetsInterface, Value};
use rocket::http::CookieJar;

Expand All @@ -14,29 +15,24 @@ const DEFAULT_INDEX_TTL: u32 = 60 * 60 * 24 * 7 * 2; // 2 weeks
/// Redis session storage using the [fred.rs](https://docs.rs/fred) crate. This is a wrapper around
/// [`RedisFredStorage`] that adds support for indexing sessions by an identifier (e.g. `user_id`).
///
/// # Requirements
/// In addition to the requirements for `RedisFredStorage`, your session data type must
/// implement [`SessionIdentifier`], and its [Id](`SessionIdentifier::Id`) type
/// must implement `ToString`. Sessions are tracked in Redis sets, with a key format of
/// `<key_prefix><identifier_name>:<id>`. e.g.: `sess:user_id:1`
/// must implement `ToString`. Sessions are tracked in Redis sets, with a key format of:
///
/// `<key_prefix><identifier_name>:<id>` (e.g.: `sess:user_id:1`)
#[derive(Builder)]
#[builder(start_fn = from_storage)]
pub struct RedisFredStorageIndexed {
#[builder(start_fn)]
/// The [`RedisFredStorage`] instance to use.
base_storage: RedisFredStorage,
#[builder(default = DEFAULT_INDEX_TTL)]
/// The TTL for the session index - should match your longest expected session duration (default: 2 weeks).
index_ttl: u32,
}

impl RedisFredStorageIndexed {
/// Create the indexed storage.
///
/// # Parameters:
/// - `base_storage`: The [`RedisFredStorage`] instance to use.
/// - `index_ttl`: The TTL for the session index - should match
/// your longest expected session duration (default: 2 weeks).
pub fn new(base_storage: RedisFredStorage, index_ttl: Option<u32>) -> Self {
Self {
base_storage,
index_ttl: index_ttl.unwrap_or(DEFAULT_INDEX_TTL),
}
}

fn session_index_key(&self, identifier_name: &str, identifier: &impl ToString) -> String {
format!(
"{}{identifier_name}:{}",
Expand Down
Loading