feat: init

This commit is contained in:
zhenyi
2026-06-07 11:30:56 +08:00
commit 563381c1ca
361 changed files with 41327 additions and 0 deletions
+60
View File
@@ -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)
}
+36
View File
@@ -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>>;
}
+12
View File
@@ -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,
};
+82
View File
@@ -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(())
}
}
+28
View File
@@ -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
}
}
+7
View File
@@ -0,0 +1,7 @@
use uuid::Uuid;
use super::SessionKey;
pub fn generate_session_key() -> SessionKey {
SessionKey(Uuid::now_v7().to_string())
}