feat: init
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
use crate::config::AppConfig;
|
||||
use crate::error::AppResult;
|
||||
|
||||
impl AppConfig {
|
||||
pub fn session_cookie_name(&self) -> AppResult<String> {
|
||||
self.get_env_or("APP_SESSION_COOKIE_NAME", "sid".to_string())
|
||||
}
|
||||
|
||||
pub fn session_cookie_secure(&self) -> AppResult<bool> {
|
||||
self.get_env_or("APP_SESSION_COOKIE_SECURE", true)
|
||||
}
|
||||
|
||||
pub fn session_cookie_http_only(&self) -> AppResult<bool> {
|
||||
self.get_env_or("APP_SESSION_COOKIE_HTTP_ONLY", true)
|
||||
}
|
||||
|
||||
pub fn session_cookie_same_site(&self) -> AppResult<String> {
|
||||
self.get_env_or("APP_SESSION_COOKIE_SAME_SITE", "Lax".to_string())
|
||||
}
|
||||
|
||||
pub fn session_cookie_path(&self) -> AppResult<String> {
|
||||
self.get_env_or("APP_SESSION_COOKIE_PATH", "/".to_string())
|
||||
}
|
||||
|
||||
pub fn session_cookie_domain(&self) -> AppResult<Option<String>> {
|
||||
self.get_env::<String>("APP_SESSION_COOKIE_DOMAIN")
|
||||
}
|
||||
|
||||
pub fn session_ttl_secs(&self) -> AppResult<u64> {
|
||||
self.get_env_or("APP_SESSION_TTL_SECS", 86400)
|
||||
}
|
||||
|
||||
pub fn session_max_age_secs(&self) -> AppResult<Option<u64>> {
|
||||
self.get_env::<u64>("APP_SESSION_MAX_AGE_SECS")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
pub mod config;
|
||||
#[allow(clippy::module_inception)]
|
||||
pub mod session;
|
||||
pub mod storage;
|
||||
|
||||
pub use self::{
|
||||
session::{Session, SessionState, SessionStatus, SessionUser},
|
||||
storage::{RedisSessionStore, SessionKey, SessionStore, generate_session_key},
|
||||
};
|
||||
@@ -0,0 +1,195 @@
|
||||
use std::cell::{Ref, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
use serde::Serialize;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde_json::{Map, Value};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::error::AppError;
|
||||
|
||||
const SESSION_USER_KEY: &str = "session:user_uid";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Session(Rc<RefCell<SessionInner>>);
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub enum SessionStatus {
|
||||
Changed,
|
||||
Purged,
|
||||
Renewed,
|
||||
#[default]
|
||||
Unchanged,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SessionInner {
|
||||
state: Map<String, Value>,
|
||||
status: SessionStatus,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn from_state(state: Map<String, Value>) -> Self {
|
||||
Self(Rc::new(RefCell::new(SessionInner {
|
||||
state,
|
||||
status: SessionStatus::Unchanged,
|
||||
})))
|
||||
}
|
||||
|
||||
pub fn get<T: DeserializeOwned>(&self, key: &str) -> Result<Option<T>, AppError> {
|
||||
if let Some(value) = self.0.borrow().state.get(key) {
|
||||
Ok(Some(serde_json::from_value(value.clone())?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn contains_key(&self, key: &str) -> bool {
|
||||
self.0.borrow().state.contains_key(key)
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> Ref<'_, Map<String, Value>> {
|
||||
Ref::map(self.0.borrow(), |inner| &inner.state)
|
||||
}
|
||||
|
||||
pub fn status(&self) -> SessionStatus {
|
||||
Ref::map(self.0.borrow(), |inner| &inner.status).clone()
|
||||
}
|
||||
|
||||
pub fn insert<T: Serialize>(&self, key: impl Into<String>, value: T) -> Result<(), AppError> {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
|
||||
if inner.status != SessionStatus::Purged {
|
||||
if inner.status != SessionStatus::Renewed {
|
||||
inner.status = SessionStatus::Changed;
|
||||
}
|
||||
let val = serde_json::to_value(&value)?;
|
||||
inner.state.insert(key.into(), val);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update<T: Serialize + DeserializeOwned, F>(
|
||||
&self,
|
||||
key: impl Into<String>,
|
||||
updater: F,
|
||||
) -> Result<(), AppError>
|
||||
where
|
||||
F: FnOnce(T) -> T,
|
||||
{
|
||||
let mut inner = self.0.borrow_mut();
|
||||
let key_str = key.into();
|
||||
|
||||
if let Some(val) = inner.state.get(&key_str) {
|
||||
if inner.status == SessionStatus::Purged {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let value: T = serde_json::from_value(val.clone())?;
|
||||
let updated = serde_json::to_value(updater(value))?;
|
||||
|
||||
if inner.status != SessionStatus::Renewed {
|
||||
inner.status = SessionStatus::Changed;
|
||||
}
|
||||
inner.state.insert(key_str, updated);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_or<T: Serialize + DeserializeOwned, F>(
|
||||
&self,
|
||||
key: &str,
|
||||
default: T,
|
||||
updater: F,
|
||||
) -> Result<(), AppError>
|
||||
where
|
||||
F: FnOnce(T) -> T,
|
||||
{
|
||||
if self.contains_key(key) {
|
||||
self.update(key, updater)
|
||||
} else {
|
||||
self.insert(key, default)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&self, key: &str) -> Option<Value> {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
|
||||
if inner.status != SessionStatus::Purged {
|
||||
if inner.status != SessionStatus::Renewed {
|
||||
inner.status = SessionStatus::Changed;
|
||||
}
|
||||
return inner.state.remove(key);
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn remove_as<T: DeserializeOwned>(&self, key: &str) -> Option<Result<T, AppError>> {
|
||||
self.remove(key)
|
||||
.map(|value| serde_json::from_value(value).map_err(AppError::Json))
|
||||
}
|
||||
|
||||
pub fn clear(&self) {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
|
||||
if inner.status != SessionStatus::Purged {
|
||||
if inner.status != SessionStatus::Renewed {
|
||||
inner.status = SessionStatus::Changed;
|
||||
}
|
||||
inner.state.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn purge(&self) {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
inner.status = SessionStatus::Purged;
|
||||
inner.state.clear();
|
||||
}
|
||||
|
||||
pub fn renew(&self) {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
|
||||
if inner.status != SessionStatus::Purged {
|
||||
inner.status = SessionStatus::Renewed;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user(&self) -> Option<Uuid> {
|
||||
self.get::<Uuid>(SESSION_USER_KEY).ok().flatten()
|
||||
}
|
||||
|
||||
pub fn set_user(&self, uid: Uuid) {
|
||||
let _ = self.insert(SESSION_USER_KEY, uid);
|
||||
}
|
||||
|
||||
pub fn clear_user(&self) {
|
||||
let _ = self.remove(SESSION_USER_KEY);
|
||||
}
|
||||
|
||||
pub fn take_state(&self) -> SessionState {
|
||||
let mut inner = self.0.borrow_mut();
|
||||
std::mem::take(&mut inner.state)
|
||||
}
|
||||
|
||||
pub fn mark_unchanged(&self) {
|
||||
self.0.borrow_mut().status = SessionStatus::Unchanged;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Session {
|
||||
fn default() -> Self {
|
||||
Self(Rc::new(RefCell::new(SessionInner::default())))
|
||||
}
|
||||
}
|
||||
|
||||
pub type SessionState = Map<String, Value>;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct SessionUser(pub Uuid);
|
||||
@@ -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