use super::{PooledSessionStore, TableOperations};
use anyhow::Context;
use async_session::{chrono, serde_json, Session};
use r2d2_sqlite::{
rusqlite, rusqlite::OpenFlags, rusqlite::OptionalExtension,
SqliteConnectionManager as SessionManager,
};
use std::{path::PathBuf, str::FromStr};
pub type SessionStore = PooledSessionStore<SessionManager>;
impl TableOperations<SessionManager> for SessionStore {
fn add_table(&self) -> async_session::Result<()> {
self.connection()?
.execute(
&format!(
"
CREATE TABLE IF NOT EXISTS {:?} (
id TEXT PRIMARY KEY NOT NULL,
expires INTEGER NULL,
session TEXT NOT NULL
)
",
self.table_name
),
rusqlite::params![],
)
.context("Failed to create the table for handling sessions")
.and(Ok(()))
}
fn clean_up(&self) -> async_session::Result<()> {
self.connection()?
.execute(
&format!("DELETE from {} WHERE expires <= ?", self.table_name),
rusqlite::params![chrono::Utc::now().timestamp()],
)
.and(Ok(()))
.context("Failed to remove sessions that have expired.")
}
fn connection(&self) -> async_session::Result<r2d2::PooledConnection<SessionManager>> {
self.pool
.get()
.context("Failed to obtain a connection to the SQLite database.")
}
fn count(&self) -> async_session::Result<usize> {
self.connection()?
.query_row(
&format!("SELECT count(*) FROM {}", self.table_name),
rusqlite::params![],
|row| row.get("count(*)"),
)
.context("Could not get the count of sessions.")
}
}
#[async_trait::async_trait]
impl async_session::SessionStore for SessionStore {
async fn load_session(&self, cookie_value: String) -> async_session::Result<Option<Session>> {
let id = Session::id_from_cookie_value(&cookie_value)?;
let connection = self.connection()?;
log::trace!("Looking for the session with the ID of {:?}", id);
connection
.query_row(
&format!(
"
SELECT
session
FROM {:?}
WHERE
id = :id AND
(expires IS NULL OR expires > :expires)
",
self.table_name
),
rusqlite::named_params![
":id": id,
":expires": chrono::Utc::now().timestamp()
],
|row| row.get::<&str, String>("session"),
)
.optional()
.context("Failed to get the column 'session' from the table")
.and_then(|session_str_opt| {
if let Some(session_string) = session_str_opt {
log::trace!("Found the session #{}", id);
serde_json::from_str(&session_string)
.context("Failed to deserialize session from JSON")
} else {
log::trace!("No session was found with the ID {}", id);
Ok(None)
}
})
}
async fn store_session(&self, session: Session) -> async_session::Result<Option<String>> {
let id = session.id();
let string = serde_json::to_string(&session)?;
let connection = self.connection()?;
connection
.execute(
&format!(
"
INSERT INTO {}
(id, session, expires)
VALUES (:id, :session, :expires)
ON CONFLICT(id)
DO UPDATE SET
expires = excluded.expires,
session = excluded.session
",
self.table_name
),
rusqlite::named_params![
":id": id,
":session": string,
":expires": session.expiry().map(|e| e.timestamp())
],
)
.context("Failed to add or update the session.")
.and(Ok(session.into_cookie_value()))
}
async fn destroy_session(&self, session: Session) -> async_session::Result {
let connection = self.connection()?;
connection
.execute(
&format!("delete from {} where id = :id", self.table_name),
rusqlite::named_params![":id": session.id()],
)
.and(Ok(()))
.context("Failed to clear out the specified session.")
}
async fn clear_store(&self) -> async_session::Result {
let connection = self.connection()?;
connection
.execute(
&format!("delete from {}", self.table_name),
rusqlite::params![],
)
.and(Ok(()))
.context("Failed to clear out the sessions.")
}
}
impl SessionStore {
pub fn from_memory(table_name: String) -> async_session::Result<Self> {
Self::from_path(
PathBuf::from_str("file:memory:?mode=memory&cache=private").unwrap(),
table_name,
)
}
pub fn from_path(
session_database_path: PathBuf,
table_name: String,
) -> async_session::Result<Self> {
Self::from_pool(
r2d2::Pool::builder()
.test_on_check_out(true)
.build(SessionManager::with_flags(
SessionManager::file(session_database_path),
OpenFlags::SQLITE_OPEN_URI
| OpenFlags::SQLITE_OPEN_READ_WRITE
| OpenFlags::SQLITE_OPEN_CREATE,
))
.unwrap(),
table_name,
)
}
}