use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};
use futures::future::BoxFuture;
use serde::{de::DeserializeOwned, Serialize};
use crate::cookie::{CookieConfig, CookieService, SameSite};
pub type SessionData = HashMap<String, serde_json::Value>;
pub struct Session {
pub id: String,
pub data: RwLock<SessionData>,
pub expires_at: u64,
}
impl Session {
pub fn new_empty(id: String, ttl_secs: u64) -> Arc<Self> {
Arc::new(Self {
id,
data: RwLock::new(HashMap::new()),
expires_at: unix_now() + ttl_secs,
})
}
pub fn from_data(id: String, data: SessionData, ttl_secs: u64) -> Arc<Self> {
Arc::new(Self {
id,
data: RwLock::new(data),
expires_at: unix_now() + ttl_secs,
})
}
pub fn id(&self) -> &str {
&self.id
}
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Option<T> {
let data = self.data.read().ok()?;
serde_json::from_value(data.get(key)?.clone()).ok()
}
pub fn set<T: Serialize>(&self, key: &str, value: T) {
if let Ok(v) = serde_json::to_value(value) {
if let Ok(mut data) = self.data.write() {
data.insert(key.to_owned(), v);
}
}
}
pub fn remove(&self, key: &str) {
if let Ok(mut data) = self.data.write() {
data.remove(key);
}
}
pub fn is_expired(&self) -> bool {
unix_now() >= self.expires_at
}
pub fn data_as_json(&self) -> String {
self.data
.read()
.ok()
.and_then(|d| serde_json::to_string(&*d).ok())
.unwrap_or_else(|| "{}".to_string())
}
}
pub fn unix_now() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
pub trait SessionStore: Send + Sync + 'static {
fn load<'a>(&'a self, id: &'a str, ttl_secs: u64) -> BoxFuture<'a, Option<Arc<Session>>>;
fn save<'a>(&'a self, session: &'a Session, ttl_secs: u64) -> BoxFuture<'a, bool>;
fn delete<'a>(&'a self, id: &'a str) -> BoxFuture<'a, bool>;
fn new_id(&self) -> String;
}
pub struct SessionConfig {
pub cookie_name: &'static str,
pub ttl_secs: u64,
pub secret: String,
pub secure: bool,
pub http_only: bool,
pub same_site: SameSite,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
cookie_name: "arcly_session",
ttl_secs: 3_600,
secret: "change-session-secret-in-production".to_string(),
secure: true,
http_only: true,
same_site: SameSite::Lax,
}
}
}
pub struct SessionManager {
store: Box<dyn SessionStore>,
ttl_secs: u64,
cookie: CookieService,
}
impl SessionManager {
pub fn new(store: impl SessionStore, config: SessionConfig) -> Self {
let cookie = CookieService::new(CookieConfig {
name: config.cookie_name,
secret: config.secret,
max_age_secs: config.ttl_secs,
secure: config.secure,
http_only: config.http_only,
same_site: config.same_site,
..Default::default()
});
Self {
store: Box::new(store),
ttl_secs: config.ttl_secs,
cookie,
}
}
pub async fn load_from_headers(&self, headers: &axum::http::HeaderMap) -> Option<Arc<Session>> {
let session_id = self.cookie.extract(headers)?;
self.store.load(&session_id, self.ttl_secs).await
}
pub async fn create(&self) -> Arc<Session> {
Session::new_empty(self.store.new_id(), self.ttl_secs)
}
pub async fn save(&self, session: &Session) -> bool {
self.store.save(session, self.ttl_secs).await
}
pub async fn delete(&self, id: &str) -> bool {
self.store.delete(id).await
}
pub fn session_cookie(&self, id: &str) -> String {
self.cookie.bake(id)
}
pub fn clear_cookie(&self) -> String {
self.cookie.clear()
}
pub fn rotate_cookie_secret(&self, new_secret: &[u8], version: u64) {
self.cookie.rotate_secret(new_secret, version);
}
}