use time::OffsetDateTime;
use crate::Session;
fn now_unix() -> i64 {
OffsetDateTime::now_utc().unix_timestamp()
}
fn expiry_unix(session: &Session) -> Option<i64> {
session.expiry().map(|e| e.unix_timestamp())
}
fn parse_session(json: &str) -> Option<Session> {
serde_json::from_str::<Session>(json)
.ok()
.and_then(Session::validate)
}
#[cfg(feature = "sqlx-postgres")]
mod pg {
use super::*;
use sqlx::PgPool;
use crate::{Result, Session, SessionStore, async_trait};
#[derive(Clone, Debug)]
pub struct SqlxPostgresStore {
pool: PgPool,
table: String,
}
impl SqlxPostgresStore {
pub fn new(pool: PgPool) -> Self {
Self {
pool,
table: "saysion_sessions".to_string(),
}
}
pub async fn from_url(url: &str) -> Result<Self> {
Ok(Self::new(PgPool::connect(url).await?))
}
pub fn with_table(mut self, table: impl Into<String>) -> Self {
self.table = table.into();
self
}
pub async fn migrate(&self) -> Result {
let sql = format!(
"CREATE TABLE IF NOT EXISTS {table} (\
id TEXT PRIMARY KEY NOT NULL, \
expires BIGINT NULL, \
session TEXT NOT NULL\
)",
table = self.table
);
sqlx::query(&sql).execute(&self.pool).await?;
Ok(())
}
pub async fn cleanup(&self) -> Result {
let sql = format!(
"DELETE FROM {} WHERE expires IS NOT NULL AND expires < $1",
self.table
);
sqlx::query(&sql)
.bind(now_unix())
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn count(&self) -> Result<i64> {
let sql = format!("SELECT COUNT(*) FROM {}", self.table);
let (n,): (i64,) = sqlx::query_as(&sql).fetch_one(&self.pool).await?;
Ok(n)
}
}
#[async_trait]
impl SessionStore for SqlxPostgresStore {
async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
let id = Session::id_from_cookie_value(&cookie_value)?;
let sql = format!(
"SELECT session FROM {} WHERE id = $1 AND (expires IS NULL OR expires > $2)",
self.table
);
let row: Option<(String,)> = sqlx::query_as(&sql)
.bind(&id)
.bind(now_unix())
.fetch_optional(&self.pool)
.await?;
Ok(row.and_then(|(s,)| parse_session(&s)))
}
async fn store_session(&self, session: Session) -> Result<Option<String>> {
let id = session.id().to_string();
let json = serde_json::to_string(&session)?;
let expires = expiry_unix(&session);
let sql = format!(
"INSERT INTO {table} (id, expires, session) VALUES ($1, $2, $3) \
ON CONFLICT (id) DO UPDATE SET expires = EXCLUDED.expires, session = EXCLUDED.session",
table = self.table
);
sqlx::query(&sql)
.bind(&id)
.bind(expires)
.bind(&json)
.execute(&self.pool)
.await?;
session.reset_data_changed();
Ok(session.into_cookie_value())
}
async fn destroy_session(&self, session: Session) -> Result {
let sql = format!("DELETE FROM {} WHERE id = $1", self.table);
sqlx::query(&sql)
.bind(session.id())
.execute(&self.pool)
.await?;
Ok(())
}
async fn clear_store(&self) -> Result {
let sql = format!("DELETE FROM {}", self.table);
sqlx::query(&sql).execute(&self.pool).await?;
Ok(())
}
}
}
#[cfg(feature = "sqlx-postgres")]
pub use pg::SqlxPostgresStore;
#[cfg(feature = "sqlx-sqlite")]
#[allow(missing_docs)]
mod sqlite {
use super::*;
use sqlx::SqlitePool;
use crate::{Result, Session, SessionStore, async_trait};
#[derive(Clone, Debug)]
pub struct SqlxSqliteStore {
pool: SqlitePool,
table: String,
}
impl SqlxSqliteStore {
pub fn new(pool: SqlitePool) -> Self {
Self {
pool,
table: "saysion_sessions".to_string(),
}
}
pub async fn from_url(url: &str) -> Result<Self> {
Ok(Self::new(SqlitePool::connect(url).await?))
}
pub fn with_table(mut self, table: impl Into<String>) -> Self {
self.table = table.into();
self
}
pub async fn migrate(&self) -> Result {
let sql = format!(
"CREATE TABLE IF NOT EXISTS {table} (\
id TEXT PRIMARY KEY NOT NULL, \
expires INTEGER NULL, \
session TEXT NOT NULL\
)",
table = self.table
);
sqlx::query(&sql).execute(&self.pool).await?;
Ok(())
}
pub async fn cleanup(&self) -> Result {
let sql = format!(
"DELETE FROM {} WHERE expires IS NOT NULL AND expires < ?",
self.table
);
sqlx::query(&sql)
.bind(now_unix())
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn count(&self) -> Result<i64> {
let sql = format!("SELECT COUNT(*) FROM {}", self.table);
let (n,): (i64,) = sqlx::query_as(&sql).fetch_one(&self.pool).await?;
Ok(n)
}
}
#[async_trait]
impl SessionStore for SqlxSqliteStore {
async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
let id = Session::id_from_cookie_value(&cookie_value)?;
let sql = format!(
"SELECT session FROM {} WHERE id = ? AND (expires IS NULL OR expires > ?)",
self.table
);
let row: Option<(String,)> = sqlx::query_as(&sql)
.bind(&id)
.bind(now_unix())
.fetch_optional(&self.pool)
.await?;
Ok(row.and_then(|(s,)| parse_session(&s)))
}
async fn store_session(&self, session: Session) -> Result<Option<String>> {
let id = session.id().to_string();
let json = serde_json::to_string(&session)?;
let expires = expiry_unix(&session);
let sql = format!(
"INSERT INTO {table} (id, expires, session) VALUES (?, ?, ?) \
ON CONFLICT(id) DO UPDATE SET expires = excluded.expires, session = excluded.session",
table = self.table
);
sqlx::query(&sql)
.bind(&id)
.bind(expires)
.bind(&json)
.execute(&self.pool)
.await?;
session.reset_data_changed();
Ok(session.into_cookie_value())
}
async fn destroy_session(&self, session: Session) -> Result {
let sql = format!("DELETE FROM {} WHERE id = ?", self.table);
sqlx::query(&sql)
.bind(session.id())
.execute(&self.pool)
.await?;
Ok(())
}
async fn clear_store(&self) -> Result {
let sql = format!("DELETE FROM {}", self.table);
sqlx::query(&sql).execute(&self.pool).await?;
Ok(())
}
}
}
#[cfg(feature = "sqlx-sqlite")]
pub use sqlite::SqlxSqliteStore;
#[cfg(feature = "sqlx-mysql")]
#[allow(missing_docs)]
mod mysql {
use super::*;
use sqlx::MySqlPool;
use crate::{Result, Session, SessionStore, async_trait};
#[derive(Clone, Debug)]
pub struct SqlxMySqlStore {
pool: MySqlPool,
table: String,
}
impl SqlxMySqlStore {
pub fn new(pool: MySqlPool) -> Self {
Self {
pool,
table: "saysion_sessions".to_string(),
}
}
pub async fn from_url(url: &str) -> Result<Self> {
Ok(Self::new(MySqlPool::connect(url).await?))
}
pub fn with_table(mut self, table: impl Into<String>) -> Self {
self.table = table.into();
self
}
pub async fn migrate(&self) -> Result {
let sql = format!(
"CREATE TABLE IF NOT EXISTS {table} (\
id VARCHAR(128) PRIMARY KEY NOT NULL, \
expires BIGINT NULL, \
session TEXT NOT NULL\
)",
table = self.table
);
sqlx::query(&sql).execute(&self.pool).await?;
Ok(())
}
pub async fn cleanup(&self) -> Result {
let sql = format!(
"DELETE FROM {} WHERE expires IS NOT NULL AND expires < ?",
self.table
);
sqlx::query(&sql)
.bind(now_unix())
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn count(&self) -> Result<i64> {
let sql = format!("SELECT COUNT(*) FROM {}", self.table);
let (n,): (i64,) = sqlx::query_as(&sql).fetch_one(&self.pool).await?;
Ok(n)
}
}
#[async_trait]
impl SessionStore for SqlxMySqlStore {
async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
let id = Session::id_from_cookie_value(&cookie_value)?;
let sql = format!(
"SELECT session FROM {} WHERE id = ? AND (expires IS NULL OR expires > ?)",
self.table
);
let row: Option<(String,)> = sqlx::query_as(&sql)
.bind(&id)
.bind(now_unix())
.fetch_optional(&self.pool)
.await?;
Ok(row.and_then(|(s,)| parse_session(&s)))
}
async fn store_session(&self, session: Session) -> Result<Option<String>> {
let id = session.id().to_string();
let json = serde_json::to_string(&session)?;
let expires = expiry_unix(&session);
let sql = format!(
"INSERT INTO {table} (id, expires, session) VALUES (?, ?, ?) \
ON DUPLICATE KEY UPDATE expires = VALUES(expires), session = VALUES(session)",
table = self.table
);
sqlx::query(&sql)
.bind(&id)
.bind(expires)
.bind(&json)
.execute(&self.pool)
.await?;
session.reset_data_changed();
Ok(session.into_cookie_value())
}
async fn destroy_session(&self, session: Session) -> Result {
let sql = format!("DELETE FROM {} WHERE id = ?", self.table);
sqlx::query(&sql)
.bind(session.id())
.execute(&self.pool)
.await?;
Ok(())
}
async fn clear_store(&self) -> Result {
let sql = format!("DELETE FROM {}", self.table);
sqlx::query(&sql).execute(&self.pool).await?;
Ok(())
}
}
}
#[cfg(feature = "sqlx-mysql")]
pub use mysql::SqlxMySqlStore;