#![warn(missing_docs, unreachable_pub, unused_crate_dependencies)]
#![deny(unsafe_code)]
#![deny(clippy::all)]
#![warn(clippy::pedantic)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use async_session::{Session, SessionStore};
use deadpool_redis::redis::AsyncCommands;
use deadpool_redis::{Config, Connection, ConnectionInfo, Pool, PoolError};
pub use {async_session, deadpool_redis};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("only ascii alphanumeric and underscore is supported as prefix")]
NonAlphaNumeric,
#[error(transparent)]
DeadpoolBuild(#[from] deadpool_redis::BuildError),
#[error(transparent)]
DeadPoolConfig(#[from] deadpool_redis::ConfigError),
}
#[derive(Clone)]
pub struct RedisSessionStore {
pool: Pool,
prefix: Option<String>,
}
impl std::fmt::Debug for RedisSessionStore {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RedisSessionStore")
.field("pool", &self.pool.manager())
.field("prefix", &self.prefix)
.finish()
}
}
impl RedisSessionStore {
#[must_use]
pub fn new(pool: &Pool) -> Self {
Self {
pool: pool.clone(),
prefix: None,
}
}
pub fn from_url(url: impl Into<String>) -> Result<Self, Error> {
let pool = Config::from_url(url).builder()?.build()?;
Ok(Self { pool, prefix: None })
}
pub fn from_connection_info(connection_info: impl Into<ConnectionInfo>) -> Result<Self, Error> {
let pool = Config::from_connection_info(connection_info)
.builder()?
.build()?;
Ok(Self { pool, prefix: None })
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Result<Self, Error> {
let prefix = prefix.into();
if !prefix
.chars()
.all(|c| char::is_ascii_alphanumeric(&c) || c == '_')
{
return Err(Error::NonAlphaNumeric);
};
self.prefix = Some(prefix);
Ok(self)
}
fn key(&self, value: impl AsRef<str>) -> String {
let value = value.as_ref().to_string();
if let Some(p) = &self.prefix {
format!("{p}/{value}")
} else {
value
}
}
async fn connection(&self) -> Result<Connection, PoolError> {
self.pool.get().await
}
}
#[async_trait::async_trait]
impl SessionStore for RedisSessionStore {
async fn load_session(&self, cookie_value: String) -> async_session::Result<Option<Session>> {
let id = Session::id_from_cookie_value(&cookie_value)?;
let mut conn = self.connection().await?;
let value = conn.get::<_, Option<String>>(self.key(id)).await?;
Ok(match value {
Some(val) => serde_json::from_str(&val)?,
None => None,
})
}
async fn store_session(&self, session: Session) -> async_session::Result<Option<String>> {
let key = self.key(session.id());
let value = serde_json::to_string(&session)?;
let mut conn = self.connection().await?;
match session.expires_in() {
Some(expiry) => {
conn.set_ex(key, value, usize::try_from(expiry.as_secs())?)
.await?;
}
None => conn.set(key, value).await?,
};
Ok(session.into_cookie_value())
}
async fn destroy_session(&self, session: Session) -> async_session::Result {
let key = self.key(session.id());
let mut conn = self.connection().await?;
conn.del(key).await?;
Ok(())
}
async fn clear_store(&self) -> async_session::Result {
let mut conn = self.connection().await?;
match &self.prefix {
Some(_) => {
let keys = conn.keys::<_, Vec<String>>(self.key("*")).await?;
if !keys.is_empty() {
conn.del(keys).await?;
}
}
None => {
deadpool_redis::redis::cmd("FLUSHDB")
.query_async(&mut conn)
.await?;
}
};
Ok(())
}
}