use crate::error::{Result, TidewayError};
use crate::traits::session::{SessionData, SessionStore};
use async_trait::async_trait;
use cookie::{Cookie, Key, SameSite};
use std::sync::Arc;
#[derive(Clone)]
pub struct CookieSessionStore {
#[allow(dead_code)] key: Arc<Key>,
config: crate::session::SessionConfig,
}
impl CookieSessionStore {
pub fn new(config: &crate::session::SessionConfig) -> Result<Self> {
let key = if let Some(ref key_str) = config.encryption_key {
let key_bytes = hex::decode(key_str)
.map_err(|e| TidewayError::internal(format!("Invalid encryption key format: {}", e)))?;
if key_bytes.len() != 32 {
return Err(TidewayError::internal("Encryption key must be 32 bytes (64 hex characters)"));
}
Key::from(&key_bytes)
} else {
tracing::warn!("Using randomly generated encryption key - NOT SECURE FOR PRODUCTION");
Key::generate()
};
Ok(Self {
key: Arc::new(key),
config: config.clone(),
})
}
fn build_cookie(&self, _session_id: &str, data: &SessionData) -> Result<Cookie<'static>> {
let serialized = serde_json::to_string(data)
.map_err(|e| TidewayError::internal(format!("Failed to serialize session: {}", e)))?;
let cookie_name = self.config.cookie_name.clone();
let cookie_path = self.config.cookie_path.clone();
let cookie = Cookie::build((cookie_name, serialized))
.path(cookie_path)
.http_only(self.config.cookie_http_only)
.secure(self.config.cookie_secure)
.same_site(SameSite::Lax)
.max_age(cookie::time::Duration::seconds(
self.config.default_ttl().as_secs() as i64
))
.build();
Ok(cookie)
}
fn parse_cookie(&self, cookie_value: &str) -> Result<SessionData> {
let cookie = Cookie::parse(cookie_value)
.map_err(|e| TidewayError::internal(format!("Failed to parse cookie: {}", e)))?;
let value = cookie.value();
serde_json::from_str(value)
.map_err(|e| TidewayError::internal(format!("Failed to deserialize session: {}", e)))
}
}
#[async_trait]
impl SessionStore for CookieSessionStore {
async fn load(&self, session_id: &str) -> Result<Option<SessionData>> {
self.parse_cookie(session_id)
.map(Some)
.or_else(|e| {
if e.to_string().contains("signature") || e.to_string().contains("verify") {
Ok(None) } else {
Err(e)
}
})
}
async fn save(&self, session_id: &str, data: SessionData) -> Result<()> {
self.build_cookie(session_id, &data)?;
Ok(())
}
async fn delete(&self, _session_id: &str) -> Result<()> {
Ok(())
}
async fn cleanup_expired(&self) -> Result<usize> {
Ok(0)
}
fn is_healthy(&self) -> bool {
true
}
}