use std::sync::Arc;
use chrono::Duration;
use rainier_http::SameSite;
use rainier_support::Result;
use crate::session::SessionData;
use crate::store::SessionStore;
#[derive(Debug, Clone)]
pub struct SessionConfig {
pub cookie: String,
pub path: String,
pub domain: Option<String>,
pub secure: bool,
pub same_site: SameSite,
pub lifetime: Duration,
}
impl SessionConfig {
pub fn cookie(mut self, name: impl Into<String>) -> Self {
self.cookie = name.into();
self
}
pub fn path(mut self, path: impl Into<String>) -> Self {
self.path = path.into();
self
}
pub fn domain(mut self, domain: impl Into<String>) -> Self {
self.domain = Some(domain.into());
self
}
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
pub fn same_site(mut self, same_site: SameSite) -> Self {
self.same_site = same_site;
self
}
pub fn lifetime(mut self, lifetime: Duration) -> Self {
self.lifetime = lifetime;
self
}
}
#[derive(Clone)]
pub struct SessionManager {
store: Arc<dyn SessionStore>,
config: SessionConfig,
}
impl SessionManager {
pub fn new(store: Arc<dyn SessionStore>) -> Self {
Self { store, config: SessionConfig::default() }
}
pub fn with_config(store: Arc<dyn SessionStore>, config: SessionConfig) -> Self {
Self { store, config }
}
pub fn configure(mut self, adjust: impl FnOnce(SessionConfig) -> SessionConfig) -> Self {
self.config = adjust(self.config);
self
}
pub fn store(&self) -> &Arc<dyn SessionStore> {
&self.store
}
pub fn config(&self) -> &SessionConfig {
&self.config
}
pub fn driver(&self) -> &str {
self.store.name()
}
pub async fn read(&self, id: &str) -> Result<Option<SessionData>> {
self.store.read(id).await
}
pub async fn destroy(&self, id: &str) -> Result<()> {
self.store.destroy(id).await
}
pub async fn gc(&self) -> Result<u64> {
self.store.gc().await
}
}
impl std::fmt::Debug for SessionManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SessionManager")
.field("driver", &self.store.name())
.field("cookie", &self.config.cookie)
.field("lifetime", &self.config.lifetime)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::store::MemorySessionStore;
fn manager() -> SessionManager {
SessionManager::new(Arc::new(MemorySessionStore::default()))
}
#[test]
fn the_defaults_are_the_safe_ones() {
let config = SessionConfig::default();
assert_eq!(config.same_site, SameSite::Lax);
assert_eq!(config.path, "/");
assert!(!config.secure, "off by default so http://localhost works");
}
#[test]
fn settings_are_configurable() {
let manager = manager().configure(|config| {
config
.cookie("my_app_session")
.secure(true)
.same_site(SameSite::Strict)
.lifetime(Duration::days(14))
.domain("example.com")
});
assert_eq!(manager.config().cookie, "my_app_session");
assert!(manager.config().secure);
assert_eq!(manager.config().same_site, SameSite::Strict);
assert_eq!(manager.config().lifetime, Duration::days(14));
assert_eq!(manager.config().domain.as_deref(), Some("example.com"));
}
#[tokio::test]
async fn a_session_can_be_read_and_destroyed_without_a_request() {
let manager = manager();
let mut values = serde_json::Map::new();
values.insert("user_id".into(), 42.into());
let data = SessionData { values, flash: Vec::new() };
manager.store().write("abc", &data).await.unwrap();
assert!(manager.read("abc").await.unwrap().is_some());
manager.destroy("abc").await.unwrap();
assert!(manager.read("abc").await.unwrap().is_none());
}
#[test]
fn the_driver_is_reported() {
assert_eq!(manager().driver(), "memory");
}
}