use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::Result as AppResult;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub id: String,
pub data: HashMap<String, serde_json::Value>,
pub created_at: u64,
pub accessed_at: u64,
pub expires_at: u64,
}
impl Session {
pub fn new(ttl: Duration) -> Self {
let id = uuid::Uuid::new_v4().to_string();
Self::with_id(id, ttl)
}
pub fn with_id(id: String, ttl: Duration) -> Self {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
Self {
id,
data: HashMap::new(),
created_at: now,
accessed_at: now,
expires_at: now + ttl.as_secs(),
}
}
pub fn is_expired(&self) -> bool {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
now > self.expires_at
}
pub fn ttl_seconds(&self) -> i64 {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
self.expires_at as i64 - now as i64
}
pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
self.data.get(key)
}
pub fn set(&mut self, key: &str, value: serde_json::Value) {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
self.accessed_at = now;
self.data.insert(key.to_string(), value);
}
pub fn remove(&mut self, key: &str) {
self.data.remove(key);
}
pub fn clear(&mut self) {
self.data.clear();
}
}
#[async_trait::async_trait]
pub trait SessionStore: Send + Sync {
async fn save(&self, session: &Session) -> AppResult<()>;
async fn load(&self, id: &str) -> AppResult<Option<Session>>;
async fn destroy(&self, id: &str) -> AppResult<()>;
async fn touch(&self, id: &str) -> AppResult<()>;
}
pub struct MemorySessionStore {
sessions: Mutex<HashMap<String, Session>>,
config: SessionConfig,
}
impl MemorySessionStore {
pub fn new(config: SessionConfig) -> Self {
Self {
sessions: Mutex::new(HashMap::new()),
config,
}
}
pub async fn create(&self) -> AppResult<Session> {
let session = Session::new(self.config.ttl);
let s = session.clone();
self.save(&s).await?;
Ok(s)
}
}
#[async_trait::async_trait]
impl SessionStore for MemorySessionStore {
async fn save(&self, session: &Session) -> AppResult<()> {
let mut sessions = self.sessions.lock().map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "session lock poisoned")
})?;
sessions.insert(session.id.clone(), session.clone());
Ok(())
}
async fn load(&self, id: &str) -> AppResult<Option<Session>> {
let sessions = self.sessions.lock().map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "session lock poisoned")
})?;
Ok(sessions.get(id).cloned().filter(|s: &Session| !s.is_expired()))
}
async fn destroy(&self, id: &str) -> AppResult<()> {
let mut sessions = self.sessions.lock().map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "session lock poisoned")
})?;
sessions.remove(id);
Ok(())
}
async fn touch(&self, id: &str) -> AppResult<()> {
let mut sessions = self.sessions.lock().map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "session lock poisoned")
})?;
if let Some(s) = sessions.get_mut(id) {
s.accessed_at = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
}
Ok(())
}
}
pub struct RedisSessionStore {
redis: crate::cache::RedisCache,
config: SessionConfig,
}
impl RedisSessionStore {
pub async fn new(redis: crate::cache::RedisCache, config: SessionConfig) -> AppResult<Self> {
Ok(Self { redis, config })
}
pub async fn create(&self) -> AppResult<Session> {
let session = Session::new(self.config.ttl);
let s = session.clone();
self.save(&s).await?;
Ok(s)
}
fn session_key(&self, id: &str) -> String {
format!("{}:{}", self.config.name, id)
}
}
#[async_trait::async_trait]
impl SessionStore for RedisSessionStore {
async fn save(&self, session: &Session) -> AppResult<()> {
let key = self.session_key(&session.id);
let ttl = Duration::from_secs(session.ttl_seconds().max(0) as u64);
self.redis
.set_json(&key, session, Some(ttl))
.await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
Ok(())
}
async fn load(&self, id: &str) -> AppResult<Option<Session>> {
let key = self.session_key(id);
let session = self.redis
.get_json::<Session>(&key)
.await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
Ok(session.filter(|s| !s.is_expired()))
}
async fn destroy(&self, id: &str) -> AppResult<()> {
let key = self.session_key(id);
self.redis
.delete(&key)
.await
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
Ok(())
}
async fn touch(&self, id: &str) -> AppResult<()> {
if let Some(mut session) = self.load(id).await? {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
session.accessed_at = now;
self.save(&session).await?;
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct SessionConfig {
pub name: String,
pub ttl: Duration,
pub secure: bool,
pub http_only: bool,
pub same_site: actix_web::cookie::SameSite,
pub path: String,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
name: "kegani_sid".to_string(),
ttl: Duration::from_secs(3600 * 24),
secure: false,
http_only: true,
same_site: actix_web::cookie::SameSite::Lax,
path: "/".to_string(),
}
}
}
impl SessionConfig {
pub fn secure(mut self, secure: bool) -> Self {
self.secure = secure;
self
}
pub fn http_only(mut self, http_only: bool) -> Self {
self.http_only = http_only;
self
}
}
#[derive(Debug)]
pub enum SessionError {
NotFound,
Expired,
LockFailed,
Redis(String),
}
impl std::fmt::Display for SessionError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SessionError::NotFound => write!(f, "Session not found"),
SessionError::Expired => write!(f, "Session expired"),
SessionError::LockFailed => write!(f, "Failed to acquire session lock"),
SessionError::Redis(e) => write!(f, "Redis error: {}", e),
}
}
}
impl std::error::Error for SessionError {}