use chrono::{DateTime, Duration, Utc};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
sync::{
atomic::{AtomicBool, Ordering},
Arc, RwLock,
},
};
#[derive(Debug, Serialize, Deserialize)]
pub struct Session {
id: String,
expiry: Option<DateTime<Utc>>,
data: Arc<RwLock<HashMap<String, String>>>,
#[serde(skip)]
cookie_value: Option<String>,
#[serde(skip)]
data_changed: Arc<AtomicBool>,
#[serde(skip)]
destroy: Arc<AtomicBool>,
}
impl Clone for Session {
fn clone(&self) -> Self {
Self {
cookie_value: None,
id: self.id.clone(),
data: self.data.clone(),
expiry: self.expiry,
destroy: self.destroy.clone(),
data_changed: self.data_changed.clone(),
}
}
}
impl Default for Session {
fn default() -> Self {
Self::new()
}
}
fn generate_cookie(len: usize) -> String {
let mut key = vec![0u8; len];
rand::thread_rng().fill_bytes(&mut key);
base64::encode(key)
}
impl Session {
pub fn new() -> Self {
let cookie_value = generate_cookie(64);
let id = Session::id_from_cookie_value(&cookie_value).unwrap();
Self {
data_changed: Arc::new(AtomicBool::new(false)),
expiry: None,
data: Arc::new(RwLock::new(HashMap::default())),
cookie_value: Some(cookie_value),
id,
destroy: Arc::new(AtomicBool::new(false)),
}
}
pub fn id_from_cookie_value(string: &str) -> Result<String, base64::DecodeError> {
let decoded = base64::decode(string)?;
let hash = blake3::hash(&decoded);
Ok(base64::encode(&hash.as_bytes()))
}
pub fn destroy(&mut self) {
self.destroy.store(true, Ordering::Relaxed);
}
pub fn is_destroyed(&self) -> bool {
self.destroy.load(Ordering::Relaxed)
}
pub fn id(&self) -> &str {
&self.id
}
pub fn insert(&mut self, key: &str, value: impl Serialize) -> Result<(), serde_json::Error> {
self.insert_raw(key, serde_json::to_string(&value)?);
Ok(())
}
pub fn insert_raw(&mut self, key: &str, value: String) {
let mut data = self.data.write().unwrap();
if data.get(key) != Some(&value) {
data.insert(key.to_string(), value);
self.data_changed.store(true, Ordering::Relaxed);
}
}
pub fn get<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
let data = self.data.read().unwrap();
let string = data.get(key)?;
serde_json::from_str(string).ok()
}
pub fn get_raw(&self, key: &str) -> Option<String> {
let data = self.data.read().unwrap();
data.get(key).cloned()
}
pub fn remove(&mut self, key: &str) {
let mut data = self.data.write().unwrap();
if data.remove(key).is_some() {
self.data_changed.store(true, Ordering::Relaxed);
}
}
pub fn len(&self) -> usize {
let data = self.data.read().unwrap();
data.len()
}
pub fn regenerate(&mut self) {
let cookie_value = generate_cookie(64);
self.id = Session::id_from_cookie_value(&cookie_value).unwrap();
self.cookie_value = Some(cookie_value);
}
pub fn set_cookie_value(&mut self, cookie_value: String) {
self.cookie_value = Some(cookie_value)
}
pub fn expiry(&self) -> Option<&DateTime<Utc>> {
self.expiry.as_ref()
}
pub fn set_expiry(&mut self, expiry: DateTime<Utc>) {
self.expiry = Some(expiry);
}
pub fn expire_in(&mut self, ttl: std::time::Duration) {
self.expiry = Some(Utc::now() + Duration::from_std(ttl).unwrap());
}
pub fn is_expired(&self) -> bool {
match self.expiry {
Some(expiry) => expiry < Utc::now(),
None => false,
}
}
pub fn validate(self) -> Option<Self> {
if self.is_expired() {
None
} else {
Some(self)
}
}
pub fn data_changed(&self) -> bool {
self.data_changed.load(Ordering::Relaxed)
}
pub fn reset_data_changed(&self) {
self.data_changed.store(false, Ordering::Relaxed);
}
pub fn expires_in(&self) -> Option<std::time::Duration> {
self.expiry?.signed_duration_since(Utc::now()).to_std().ok()
}
pub fn into_cookie_value(mut self) -> Option<String> {
self.cookie_value.take()
}
}
impl PartialEq for Session {
fn eq(&self, other: &Self) -> bool {
other.id == self.id
}
}