use actix_web::HttpRequest;
use std::collections::HashSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};
pub const SESSION_COOKIE_NAME: &str = "pokedex_session";
static COUNTER: AtomicU64 = AtomicU64::new(1);
#[derive(Clone, Default)]
pub struct SessionStore {
sessions: Arc<RwLock<HashSet<String>>>,
}
impl SessionStore {
#[must_use]
pub fn new() -> Self {
Self {
sessions: Arc::new(RwLock::new(HashSet::new())),
}
}
pub fn create_session(&self) -> String {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
let token = format!("{nanos:016x}{seq:016x}");
if let Ok(mut lock) = self.sessions.write() {
lock.insert(token.clone());
}
token
}
#[must_use]
pub fn is_authenticated(&self, req: &HttpRequest) -> bool {
if let Some(cookie) = req.cookie(SESSION_COOKIE_NAME) {
let val = cookie.value();
if let Ok(lock) = self.sessions.read() {
return lock.contains(val);
}
}
false
}
pub fn destroy_session(&self, req: &HttpRequest) {
if let Some(cookie) = req.cookie(SESSION_COOKIE_NAME) {
let val = cookie.value();
if let Ok(mut lock) = self.sessions.write() {
lock.remove(val);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::cookie::Cookie;
use actix_web::test::TestRequest;
#[test]
fn test_session_lifecycle() {
let store = SessionStore::new();
let token = store.create_session();
let req_valid = TestRequest::default()
.cookie(Cookie::build(SESSION_COOKIE_NAME, token.clone()).finish())
.to_http_request();
assert!(store.is_authenticated(&req_valid));
let req_invalid = TestRequest::default()
.cookie(Cookie::build(SESSION_COOKIE_NAME, "wrong_token").finish())
.to_http_request();
assert!(!store.is_authenticated(&req_invalid));
store.destroy_session(&req_valid);
assert!(!store.is_authenticated(&req_valid));
}
}