feat: init
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
use serde::ser::{Serialize, SerializeMap, Serializer};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
use super::interface::SessionState;
|
||||
|
||||
const SESSION_STATE_FORMAT_VERSION: u8 = 1;
|
||||
|
||||
struct StoredSessionStateRef<'a> {
|
||||
state: &'a SessionState,
|
||||
}
|
||||
|
||||
impl Serialize for StoredSessionStateRef<'_> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(Some(2))?;
|
||||
map.serialize_entry("v", &SESSION_STATE_FORMAT_VERSION)?;
|
||||
map.serialize_entry("state", self.state)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_session_state(session_state: &SessionState) -> Result<String, AppError> {
|
||||
let stored = StoredSessionStateRef {
|
||||
state: session_state,
|
||||
};
|
||||
serde_json::to_string(&stored).map_err(AppError::Json)
|
||||
}
|
||||
|
||||
pub fn deserialize_session_state(value: &str) -> Result<SessionState, AppError> {
|
||||
let value: Value = serde_json::from_str(value)?;
|
||||
let Value::Object(mut obj) = value else {
|
||||
return Err(AppError::Config("invalid session state format".into()));
|
||||
};
|
||||
|
||||
if let Some(Value::Object(_)) = obj.get("state")
|
||||
&& let Some(Value::Number(v)) = obj.get("v")
|
||||
{
|
||||
let version = v
|
||||
.as_u64()
|
||||
.and_then(|n| u8::try_from(n).ok())
|
||||
.ok_or_else(|| AppError::Config("invalid session state format version".into()))?;
|
||||
|
||||
if version != SESSION_STATE_FORMAT_VERSION {
|
||||
return Err(AppError::Config(format!(
|
||||
"unsupported session state format version: {version}"
|
||||
)));
|
||||
}
|
||||
|
||||
let Some(Value::Object(state)) = obj.remove("state") else {
|
||||
return Err(AppError::Config("missing session state".into()));
|
||||
};
|
||||
return Ok(state);
|
||||
}
|
||||
|
||||
Ok(obj)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use std::future::Future;
|
||||
|
||||
use serde_json::{Map, Value};
|
||||
|
||||
use super::SessionKey;
|
||||
use crate::error::AppError;
|
||||
|
||||
pub type SessionState = Map<String, Value>;
|
||||
|
||||
pub trait SessionStore {
|
||||
fn load(
|
||||
&self,
|
||||
session_key: &SessionKey,
|
||||
) -> impl Future<Output = Result<Option<SessionState>, AppError>>;
|
||||
|
||||
fn save(
|
||||
&self,
|
||||
session_state: SessionState,
|
||||
ttl_secs: u64,
|
||||
) -> impl Future<Output = Result<SessionKey, AppError>>;
|
||||
|
||||
fn update(
|
||||
&self,
|
||||
session_key: SessionKey,
|
||||
session_state: SessionState,
|
||||
ttl_secs: u64,
|
||||
) -> impl Future<Output = Result<SessionKey, AppError>>;
|
||||
|
||||
fn update_ttl(
|
||||
&self,
|
||||
session_key: &SessionKey,
|
||||
ttl_secs: u64,
|
||||
) -> impl Future<Output = Result<(), AppError>>;
|
||||
|
||||
fn delete(&self, session_key: &SessionKey) -> impl Future<Output = Result<(), AppError>>;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
mod format;
|
||||
mod interface;
|
||||
mod redis;
|
||||
mod session_key;
|
||||
mod utils;
|
||||
|
||||
pub use self::{
|
||||
interface::{SessionState, SessionStore},
|
||||
redis::RedisSessionStore,
|
||||
session_key::SessionKey,
|
||||
utils::generate_session_key,
|
||||
};
|
||||
@@ -0,0 +1,82 @@
|
||||
use crate::cache::redis::AppRedis;
|
||||
use crate::error::AppError;
|
||||
|
||||
use super::SessionKey;
|
||||
use super::format::{deserialize_session_state, serialize_session_state};
|
||||
use super::interface::{SessionState, SessionStore};
|
||||
use super::utils::generate_session_key;
|
||||
|
||||
pub struct RedisSessionStore {
|
||||
redis: AppRedis,
|
||||
}
|
||||
|
||||
impl RedisSessionStore {
|
||||
pub fn new(redis: AppRedis) -> Self {
|
||||
Self { redis }
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionStore for RedisSessionStore {
|
||||
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, AppError> {
|
||||
let mut conn = self.redis.get_connection()?;
|
||||
let value: Option<String> = redis::cmd("GET")
|
||||
.arg(session_key.as_ref())
|
||||
.query(&mut *conn.inner_mut())
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
match value {
|
||||
None => Ok(None),
|
||||
Some(v) => Ok(Some(deserialize_session_state(&v)?)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn save(
|
||||
&self,
|
||||
session_state: SessionState,
|
||||
ttl_secs: u64,
|
||||
) -> Result<SessionKey, AppError> {
|
||||
let body = serialize_session_state(&session_state)?;
|
||||
let session_key = generate_session_key();
|
||||
let mut conn = self.redis.get_connection()?;
|
||||
redis::cmd("SETEX")
|
||||
.arg(session_key.as_ref())
|
||||
.arg(ttl_secs)
|
||||
.arg(&body)
|
||||
.query::<()>(&mut *conn.inner_mut())?;
|
||||
Ok(session_key)
|
||||
}
|
||||
|
||||
async fn update(
|
||||
&self,
|
||||
session_key: SessionKey,
|
||||
session_state: SessionState,
|
||||
ttl_secs: u64,
|
||||
) -> Result<SessionKey, AppError> {
|
||||
let body = serialize_session_state(&session_state)?;
|
||||
let mut conn = self.redis.get_connection()?;
|
||||
redis::cmd("SETEX")
|
||||
.arg(session_key.as_ref())
|
||||
.arg(ttl_secs)
|
||||
.arg(&body)
|
||||
.query::<()>(&mut *conn.inner_mut())?;
|
||||
Ok(session_key)
|
||||
}
|
||||
|
||||
async fn update_ttl(&self, session_key: &SessionKey, ttl_secs: u64) -> Result<(), AppError> {
|
||||
let mut conn = self.redis.get_connection()?;
|
||||
redis::cmd("EXPIRE")
|
||||
.arg(session_key.as_ref())
|
||||
.arg(ttl_secs)
|
||||
.query::<()>(&mut *conn.inner_mut())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete(&self, session_key: &SessionKey) -> Result<(), AppError> {
|
||||
let mut conn = self.redis.get_connection()?;
|
||||
redis::cmd("DEL")
|
||||
.arg(session_key.as_ref())
|
||||
.query::<()>(&mut *conn.inner_mut())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SessionKey(pub String);
|
||||
|
||||
impl TryFrom<String> for SessionKey {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(val: String) -> Result<Self, Self::Error> {
|
||||
if val.len() > 4064 {
|
||||
return Err("session key exceeds 4064 bytes");
|
||||
}
|
||||
if val.contains('\0') {
|
||||
return Err("session key contains null bytes");
|
||||
}
|
||||
Ok(SessionKey(val))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for SessionKey {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SessionKey> for String {
|
||||
fn from(key: SessionKey) -> Self {
|
||||
key.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::SessionKey;
|
||||
|
||||
pub fn generate_session_key() -> SessionKey {
|
||||
SessionKey(Uuid::now_v7().to_string())
|
||||
}
|
||||
Reference in New Issue
Block a user