use std::collections::HashMap;
use async_trait::async_trait;
use sha2::{Digest, Sha256};
use sqlx::Row;
use sqlx::types::Json;
use tower_sessions::session::{Id, Record};
use tower_sessions::session_store::{self, ExpiredDeletion, SessionStore};
use crate::database::DatabaseConfig;
use super::dialect::{SessionDb, SessionPool, restored_time, sql, stored_time};
use super::error::SessionStoreError;
use super::migrate;
const CREATE_ATTEMPTS: u32 = 8;
const CONNECTION_CEILING: u32 = 4;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct DbSessionStore {
pool: SessionPool,
}
impl DbSessionStore {
#[must_use]
pub fn new(pool: SessionPool) -> Self {
Self { pool }
}
#[must_use]
pub fn connect_lazy(config: &DatabaseConfig) -> Self {
let pool_config = config.pool_config();
let pool = sqlx::pool::PoolOptions::<SessionDb>::new()
.max_connections(pool_config.get_max_connections().min(CONNECTION_CEILING))
.min_connections(0)
.acquire_timeout(pool_config.get_acquire_timeout())
.idle_timeout(pool_config.get_idle_timeout())
.max_lifetime(pool_config.get_max_lifetime())
.connect_lazy_with(config.connect_options());
Self { pool }
}
#[must_use]
pub fn pool(&self) -> &SessionPool {
&self.pool
}
pub async fn migrate(&self) -> Result<(), SessionStoreError> {
migrate::apply(&self.pool).await
}
pub async fn sweep_expired(&self) -> Result<u64, SessionStoreError> {
let result = sqlx::query(sql::DELETE_EXPIRED).execute(&self.pool).await?;
Ok(result.rows_affected())
}
async fn write(
&self,
statement: &'static str,
record: &Record,
) -> Result<bool, SessionStoreError> {
let expires_at = stored_time(record.expiry_date)?;
let result = sqlx::query(statement)
.bind(digest_of(&record.id))
.bind(Json(&record.data))
.bind(expires_at)
.execute(&self.pool)
.await?;
Ok(result.rows_affected() > 0)
}
async fn read(&self, session_id: &Id) -> Result<Option<Record>, SessionStoreError> {
let Some(row) = sqlx::query(sql::LOAD)
.bind(digest_of(session_id))
.fetch_optional(&self.pool)
.await?
else {
return Ok(None);
};
let data: Json<HashMap<String, serde_json::Value>> = row
.try_get(0)
.map_err(|error| SessionStoreError::Decode(error.to_string()))?;
let expires_at = row.try_get(1)?;
Ok(Some(Record {
id: *session_id,
data: data.0,
expiry_date: restored_time(expires_at)?,
}))
}
}
fn digest_of(id: &Id) -> Vec<u8> {
Sha256::digest(id.0.to_le_bytes()).to_vec()
}
#[async_trait]
impl SessionStore for DbSessionStore {
async fn create(&self, record: &mut Record) -> session_store::Result<()> {
for _ in 0..CREATE_ATTEMPTS {
if self.write(sql::INSERT_NEW, record).await? {
return Ok(());
}
record.id = Id::default();
}
Err(SessionStoreError::IdCollision {
attempts: CREATE_ATTEMPTS,
}
.into())
}
async fn save(&self, record: &Record) -> session_store::Result<()> {
self.write(sql::UPSERT, record).await?;
Ok(())
}
async fn load(&self, session_id: &Id) -> session_store::Result<Option<Record>> {
Ok(self.read(session_id).await?)
}
async fn delete(&self, session_id: &Id) -> session_store::Result<()> {
sqlx::query(sql::DELETE)
.bind(digest_of(session_id))
.execute(&self.pool)
.await
.map_err(SessionStoreError::from)?;
Ok(())
}
}
#[async_trait]
impl ExpiredDeletion for DbSessionStore {
async fn delete_expired(&self) -> session_store::Result<()> {
self.sweep_expired().await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_stored_key_is_not_the_session_id() {
let id = Id(0x0123_4567_89ab_cdef_0123_4567_89ab_cdef);
let digest = digest_of(&id);
assert_eq!(digest.len(), 32);
assert_ne!(digest.as_slice(), &id.0.to_le_bytes()[..]);
}
#[test]
fn the_same_id_always_hashes_to_the_same_key() {
let id = Id(-42);
assert_eq!(digest_of(&id), digest_of(&id));
}
#[test]
fn different_ids_hash_to_different_keys() {
assert_ne!(digest_of(&Id(1)), digest_of(&Id(2)));
}
}