siderite_sessions/
cache_store.rs1use crate::id::id_hash;
4use crate::store::{SessionRecord, SessionStore, StoreError};
5use async_trait::async_trait;
6use siderite_cache::{Cache, CacheExt};
7use std::sync::Arc;
8use std::time::Duration;
9
10const DEFAULT_PREFIX: &str = "siderite:session:";
11const DEFAULT_TOMBSTONE_TTL: Duration = Duration::from_secs(24 * 60 * 60);
14
15pub struct CacheStore<C: Cache> {
42 cache: Arc<C>,
43 prefix: String,
44 tombstone_ttl: Duration,
45}
46
47impl<C: Cache> Clone for CacheStore<C> {
48 fn clone(&self) -> Self {
49 Self {
50 cache: Arc::clone(&self.cache),
51 prefix: self.prefix.clone(),
52 tombstone_ttl: self.tombstone_ttl,
53 }
54 }
55}
56
57impl<C: Cache> std::fmt::Debug for CacheStore<C> {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 f.debug_struct("CacheStore")
60 .field("prefix", &self.prefix)
61 .finish_non_exhaustive()
62 }
63}
64
65impl<C: Cache> CacheStore<C> {
66 #[must_use]
68 pub fn new(cache: C) -> Self {
69 Self {
70 cache: Arc::new(cache),
71 prefix: DEFAULT_PREFIX.to_owned(),
72 tombstone_ttl: DEFAULT_TOMBSTONE_TTL,
73 }
74 }
75
76 #[must_use]
79 pub fn with_tombstone_ttl(mut self, ttl: Duration) -> Self {
80 self.tombstone_ttl = ttl;
81 self
82 }
83
84 #[must_use]
86 pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
87 self.prefix = prefix.into();
88 self
89 }
90
91 fn record_key(&self, id_hash: &str) -> String {
92 format!("{}s:{id_hash}", self.prefix)
93 }
94
95 fn tombstone_key(&self, id_hash: &str) -> String {
96 format!("{}d:{id_hash}", self.prefix)
97 }
98
99 async fn deleted(&self, id_hash: &str) -> Result<bool, StoreError> {
100 self.cache
101 .get(&self.tombstone_key(id_hash))
102 .await
103 .map(|found| found.is_some())
104 .map_err(|_| unavailable())
105 }
106
107 fn epoch_key(&self, user: &str) -> String {
108 format!("{}e:{}", self.prefix, id_hash(user))
109 }
110
111 async fn epoch(&self, user: &str) -> Result<u64, StoreError> {
112 let bytes = self
113 .cache
114 .get(&self.epoch_key(user))
115 .await
116 .map_err(|_| unavailable())?;
117 let Some(bytes) = bytes else {
118 return Ok(0);
119 };
120 std::str::from_utf8(&bytes)
121 .ok()
122 .and_then(|text| text.parse::<u64>().ok())
123 .ok_or_else(|| StoreError::new("corrupt revocation epoch"))
124 }
125}
126
127fn unavailable() -> StoreError {
128 StoreError::new("cache backend unavailable")
129}
130
131#[async_trait]
132impl<C: Cache> SessionStore for CacheStore<C> {
133 async fn load(
134 &self,
135 id_hash: &str,
136 ) -> Result<Option<SessionRecord>, StoreError> {
137 let key = self.record_key(id_hash);
138 let record = self
139 .cache
140 .get_json::<SessionRecord>(&key)
141 .await
142 .map_err(|_| unavailable())?;
143 let Some(record) = record else {
144 return Ok(None);
145 };
146 if self.deleted(id_hash).await? {
147 return Ok(None);
148 }
149 if let Some(user) = &record.user
150 && self.epoch(user).await? != record.auth_epoch
151 {
152 return Ok(None);
153 }
154 Ok(Some(record))
155 }
156
157 async fn save(
158 &self,
159 id_hash: &str,
160 record: &SessionRecord,
161 ttl: Duration,
162 ) -> Result<(), StoreError> {
163 let key = self.record_key(id_hash);
164 self.cache
165 .set_json(&key, record, Some(ttl))
166 .await
167 .map_err(|_| unavailable())
168 }
169
170 async fn update(
171 &self,
172 id_hash: &str,
173 record: &SessionRecord,
174 ttl: Duration,
175 ) -> Result<bool, StoreError> {
176 let key = self.record_key(id_hash);
177 let stored = self.cache.get(&key).await.map_err(|_| unavailable())?;
178 if stored.is_none() || self.deleted(id_hash).await? {
179 return Ok(false);
180 }
181 let ttl = ttl.min(self.tombstone_ttl);
182 self.cache
183 .set_json(&key, record, Some(ttl))
184 .await
185 .map(|()| true)
186 .map_err(|_| unavailable())
187 }
188
189 async fn delete(&self, id_hash: &str) -> Result<(), StoreError> {
190 let tombstone = self.tombstone_key(id_hash);
193 let ttl = Some(self.tombstone_ttl);
194 self.cache
195 .set(&tombstone, b"1".to_vec(), ttl)
196 .await
197 .map_err(|_| unavailable())?;
198 let key = self.record_key(id_hash);
199 self.cache
200 .delete(&key)
201 .await
202 .map(|_| ())
203 .map_err(|_| unavailable())
204 }
205
206 async fn revoke_user(&self, user: &str) -> Result<(), StoreError> {
207 self.cache
208 .increment(&self.epoch_key(user), 1)
209 .await
210 .map(|_| ())
211 .map_err(|_| unavailable())
212 }
213
214 async fn auth_epoch(&self, user: &str) -> Result<u64, StoreError> {
215 self.epoch(user).await
216 }
217}