modo/auth/session/
extractor.rs1use std::sync::atomic::{AtomicBool, Ordering};
2use std::sync::{Arc, Mutex};
3
4use axum::extract::FromRequestParts;
5use http::request::Parts;
6use serde::Serialize;
7use serde::de::DeserializeOwned;
8
9use crate::error::{Error, HttpError};
10
11use super::meta::SessionMeta;
12use super::store::{SessionData, Store};
13use super::token::SessionToken;
14
15#[derive(Clone)]
16pub(crate) enum SessionAction {
17 None,
18 Set(SessionToken),
19 Remove,
20}
21
22pub(crate) struct SessionState {
23 pub store: Store,
24 pub meta: SessionMeta,
25 pub current: Mutex<Option<SessionData>>,
26 pub dirty: AtomicBool,
27 pub action: Mutex<SessionAction>,
28}
29
30pub struct Session {
46 state: Arc<SessionState>,
47}
48
49impl<S: Send + Sync> FromRequestParts<S> for Session {
50 type Rejection = Error;
51
52 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
53 let state = parts
54 .extensions
55 .get::<Arc<SessionState>>()
56 .cloned()
57 .ok_or_else(|| Error::internal("Session extractor requires session middleware"))?;
58
59 Ok(Self { state })
60 }
61}
62
63impl Session {
64 pub fn user_id(&self) -> Option<String> {
68 let guard = self.state.current.lock().expect("session mutex poisoned");
69 guard.as_ref().map(|s| s.user_id.clone())
70 }
71
72 pub fn get<T: DeserializeOwned>(&self, key: &str) -> crate::Result<Option<T>> {
80 let guard = self.state.current.lock().expect("session mutex poisoned");
81 let session = match guard.as_ref() {
82 Some(s) => s,
83 None => return Ok(None),
84 };
85 match session.data.get(key) {
86 Some(v) => {
87 let val = serde_json::from_value(v.clone()).map_err(|e| {
88 Error::internal(format!("deserialize session key '{key}': {e}"))
89 })?;
90 Ok(Some(val))
91 }
92 None => Ok(None),
93 }
94 }
95
96 pub fn is_authenticated(&self) -> bool {
98 let guard = self.state.current.lock().expect("session mutex poisoned");
99 guard.is_some()
100 }
101
102 pub fn current(&self) -> Option<SessionData> {
104 let guard = self.state.current.lock().expect("session mutex poisoned");
105 guard.clone()
106 }
107
108 pub fn set<T: Serialize>(&self, key: &str, value: &T) -> crate::Result<()> {
120 let mut guard = self.state.current.lock().expect("session mutex poisoned");
121 let session = match guard.as_mut() {
122 Some(s) => s,
123 None => return Ok(()), };
125 if let serde_json::Value::Object(ref mut map) = session.data {
126 map.insert(
127 key.to_string(),
128 serde_json::to_value(value)
129 .map_err(|e| Error::internal(format!("serialize session value: {e}")))?,
130 );
131 self.state.dirty.store(true, Ordering::SeqCst);
132 }
133 Ok(())
134 }
135
136 pub fn remove_key(&self, key: &str) {
142 let mut guard = self.state.current.lock().expect("session mutex poisoned");
143 if let Some(ref mut session) = *guard
144 && let serde_json::Value::Object(ref mut map) = session.data
145 && map.remove(key).is_some()
146 {
147 self.state.dirty.store(true, Ordering::SeqCst);
148 }
149 }
150
151 pub async fn authenticate(&self, user_id: &str) -> crate::Result<()> {
164 self.authenticate_with(user_id, serde_json::json!({})).await
165 }
166
167 pub async fn authenticate_with(
177 &self,
178 user_id: &str,
179 data: serde_json::Value,
180 ) -> crate::Result<()> {
181 let existing_id = {
183 let current = self.state.current.lock().expect("session mutex poisoned");
184 current.as_ref().map(|s| s.id.clone())
185 };
186 if let Some(id) = existing_id {
187 self.state.store.destroy(&id).await?;
188 }
189
190 let (session_data, token) = self
191 .state
192 .store
193 .create(&self.state.meta, user_id, Some(data))
194 .await?;
195
196 *self.state.current.lock().expect("session mutex poisoned") = Some(session_data);
197 *self.state.action.lock().expect("session mutex poisoned") = SessionAction::Set(token);
198 self.state.dirty.store(false, Ordering::SeqCst);
199 Ok(())
200 }
201
202 pub async fn rotate(&self) -> crate::Result<()> {
212 let session_id = {
213 let current = self.state.current.lock().expect("session mutex poisoned");
214 let session = current
215 .as_ref()
216 .ok_or_else(|| Error::from(HttpError::Unauthorized))?;
217 session.id.clone()
218 };
219
220 let new_token = self.state.store.rotate_token(&session_id).await?;
221
222 let now = chrono::Utc::now();
223 let new_expires =
224 now + chrono::Duration::seconds(self.state.store.config().session_ttl_secs as i64);
225 self.state
226 .store
227 .touch(&session_id, now, new_expires)
228 .await?;
229
230 *self.state.action.lock().expect("session mutex poisoned") = SessionAction::Set(new_token);
231 Ok(())
232 }
233
234 pub async fn logout(&self) -> crate::Result<()> {
242 let existing_id = {
243 let current = self.state.current.lock().expect("session mutex poisoned");
244 current.as_ref().map(|s| s.id.clone())
245 };
246 if let Some(id) = existing_id {
247 self.state.store.destroy(&id).await?;
248 }
249 *self.state.action.lock().expect("session mutex poisoned") = SessionAction::Remove;
250 *self.state.current.lock().expect("session mutex poisoned") = None;
251 Ok(())
252 }
253
254 pub async fn logout_all(&self) -> crate::Result<()> {
262 let existing_user_id = {
263 let current = self.state.current.lock().expect("session mutex poisoned");
264 current.as_ref().map(|s| s.user_id.clone())
265 };
266 if let Some(user_id) = existing_user_id {
267 self.state.store.destroy_all_for_user(&user_id).await?;
268 }
269 *self.state.action.lock().expect("session mutex poisoned") = SessionAction::Remove;
270 *self.state.current.lock().expect("session mutex poisoned") = None;
271 Ok(())
272 }
273
274 pub async fn logout_other(&self) -> crate::Result<()> {
283 let (user_id, session_id) = {
284 let current = self.state.current.lock().expect("session mutex poisoned");
285 let session = current
286 .as_ref()
287 .ok_or_else(|| Error::from(HttpError::Unauthorized))?;
288 (session.user_id.clone(), session.id.clone())
289 };
290 self.state
291 .store
292 .destroy_all_except(&user_id, &session_id)
293 .await
294 }
295
296 pub async fn list_my_sessions(&self) -> crate::Result<Vec<SessionData>> {
305 let user_id = {
306 let current = self.state.current.lock().expect("session mutex poisoned");
307 let session = current
308 .as_ref()
309 .ok_or_else(|| Error::from(HttpError::Unauthorized))?;
310 session.user_id.clone()
311 };
312 self.state.store.list_for_user(&user_id).await
313 }
314
315 pub async fn revoke(&self, id: &str) -> crate::Result<()> {
327 let current_user_id = {
328 let current = self.state.current.lock().expect("session mutex poisoned");
329 let session = current
330 .as_ref()
331 .ok_or_else(|| Error::from(HttpError::Unauthorized))?;
332 session.user_id.clone()
333 };
334
335 let target = self
336 .state
337 .store
338 .read(id)
339 .await?
340 .ok_or_else(|| Error::from(HttpError::NotFound))?;
341
342 if target.user_id != current_user_id {
343 return Err(Error::from(HttpError::NotFound));
344 }
345
346 self.state.store.destroy(id).await
347 }
348}