modo/auth/session/cookie/
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 crate::auth::session::data::Session;
12use crate::auth::session::meta::SessionMeta;
13use crate::auth::session::store::SessionData;
14use crate::auth::session::token::SessionToken;
15
16use super::CookieSessionService;
17
18#[derive(Clone)]
19pub(crate) enum SessionAction {
20 None,
21 Set(SessionToken),
22 Remove,
23}
24
25pub(crate) struct SessionState {
26 pub service: CookieSessionService,
27 pub meta: SessionMeta,
28 pub current: Mutex<Option<SessionData>>,
29 pub dirty: AtomicBool,
30 pub action: Mutex<SessionAction>,
31}
32
33pub struct CookieSession {
49 pub(crate) state: Arc<SessionState>,
50}
51
52impl<S: Send + Sync> FromRequestParts<S> for CookieSession {
53 type Rejection = Error;
54
55 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
56 let state = parts
57 .extensions
58 .get::<Arc<SessionState>>()
59 .cloned()
60 .ok_or_else(|| {
61 Error::internal("CookieSession requires CookieSessionLayer")
62 .with_code("auth:middleware_missing")
63 })?;
64 Ok(Self { state })
65 }
66}
67
68impl CookieSession {
69 pub fn current(&self) -> Option<Session> {
73 let guard = self.state.current.lock().expect("session mutex poisoned");
74 guard.as_ref().map(|raw| Session::from(raw.clone()))
75 }
76
77 pub fn is_authenticated(&self) -> bool {
79 let guard = self.state.current.lock().expect("session mutex poisoned");
80 guard.is_some()
81 }
82
83 pub fn user_id(&self) -> Option<String> {
85 let guard = self.state.current.lock().expect("session mutex poisoned");
86 guard.as_ref().map(|s| s.user_id.clone())
87 }
88
89 pub fn get<T: DeserializeOwned>(&self, key: &str) -> crate::Result<Option<T>> {
97 let guard = self.state.current.lock().expect("session mutex poisoned");
98 let session = match guard.as_ref() {
99 Some(s) => s,
100 None => return Ok(None),
101 };
102 match session.data.get(key) {
103 Some(v) => {
104 let val = serde_json::from_value(v.clone()).map_err(|e| {
105 Error::internal(format!("deserialize session key '{key}': {e}"))
106 })?;
107 Ok(Some(val))
108 }
109 None => Ok(None),
110 }
111 }
112
113 pub fn set<T: Serialize>(&self, key: &str, value: &T) -> crate::Result<()> {
125 let mut guard = self.state.current.lock().expect("session mutex poisoned");
126 let session = match guard.as_mut() {
127 Some(s) => s,
128 None => return Ok(()), };
130 if let serde_json::Value::Object(ref mut map) = session.data {
131 map.insert(
132 key.to_string(),
133 serde_json::to_value(value)
134 .map_err(|e| Error::internal(format!("serialize session value: {e}")))?,
135 );
136 self.state.dirty.store(true, Ordering::SeqCst);
137 }
138 Ok(())
139 }
140
141 pub fn remove_key(&self, key: &str) {
147 let mut guard = self.state.current.lock().expect("session mutex poisoned");
148 if let Some(ref mut session) = *guard
149 && let serde_json::Value::Object(ref mut map) = session.data
150 && map.remove(key).is_some()
151 {
152 self.state.dirty.store(true, Ordering::SeqCst);
153 }
154 }
155
156 pub async fn authenticate(&self, user_id: &str) -> crate::Result<()> {
168 self.authenticate_with(user_id, serde_json::json!({})).await
169 }
170
171 pub async fn authenticate_with(
181 &self,
182 user_id: &str,
183 data: serde_json::Value,
184 ) -> crate::Result<()> {
185 let existing_id = {
187 let current = self.state.current.lock().expect("session mutex poisoned");
188 current.as_ref().map(|s| s.id.clone())
189 };
190 if let Some(id) = existing_id {
191 self.state.service.store().destroy(&id).await?;
192 }
193
194 let (session_data, token) = self
195 .state
196 .service
197 .store()
198 .create(&self.state.meta, user_id, Some(data))
199 .await?;
200
201 *self.state.current.lock().expect("session mutex poisoned") = Some(session_data);
202 *self.state.action.lock().expect("session mutex poisoned") = SessionAction::Set(token);
203 self.state.dirty.store(false, Ordering::SeqCst);
204 Ok(())
205 }
206
207 pub async fn rotate(&self) -> crate::Result<()> {
217 let session_id = {
218 let current = self.state.current.lock().expect("session mutex poisoned");
219 let session = current
220 .as_ref()
221 .ok_or_else(|| Error::from(HttpError::Unauthorized))?;
222 session.id.clone()
223 };
224
225 let store = self.state.service.store();
226 let new_token = store.rotate_token(&session_id).await?;
227
228 let now = chrono::Utc::now();
229 let new_expires =
230 now + chrono::Duration::seconds(self.state.service.config().session_ttl_secs as i64);
231 store.touch(&session_id, now, new_expires).await?;
232
233 *self.state.action.lock().expect("session mutex poisoned") = SessionAction::Set(new_token);
234 Ok(())
235 }
236
237 pub async fn logout(&self) -> crate::Result<()> {
245 let existing_id = {
246 let current = self.state.current.lock().expect("session mutex poisoned");
247 current.as_ref().map(|s| s.id.clone())
248 };
249 if let Some(id) = existing_id {
250 self.state.service.store().destroy(&id).await?;
251 }
252 *self.state.action.lock().expect("session mutex poisoned") = SessionAction::Remove;
253 *self.state.current.lock().expect("session mutex poisoned") = None;
254 Ok(())
255 }
256
257 pub async fn logout_all(&self) -> crate::Result<()> {
265 let existing_user_id = {
266 let current = self.state.current.lock().expect("session mutex poisoned");
267 current.as_ref().map(|s| s.user_id.clone())
268 };
269 if let Some(user_id) = existing_user_id {
270 self.state
271 .service
272 .store()
273 .destroy_all_for_user(&user_id)
274 .await?;
275 }
276 *self.state.action.lock().expect("session mutex poisoned") = SessionAction::Remove;
277 *self.state.current.lock().expect("session mutex poisoned") = None;
278 Ok(())
279 }
280
281 pub async fn logout_other(&self) -> crate::Result<()> {
290 let (user_id, session_id) = {
291 let current = self.state.current.lock().expect("session mutex poisoned");
292 let session = current
293 .as_ref()
294 .ok_or_else(|| Error::from(HttpError::Unauthorized))?;
295 (session.user_id.clone(), session.id.clone())
296 };
297 self.state
298 .service
299 .store()
300 .destroy_all_except(&user_id, &session_id)
301 .await
302 }
303
304 pub async fn list_my_sessions(&self) -> crate::Result<Vec<Session>> {
313 let user_id = {
314 let current = self.state.current.lock().expect("session mutex poisoned");
315 let session = current
316 .as_ref()
317 .ok_or_else(|| Error::from(HttpError::Unauthorized))?;
318 session.user_id.clone()
319 };
320 self.state.service.list(&user_id).await
321 }
322
323 pub async fn revoke(&self, id: &str) -> crate::Result<()> {
335 let current_user_id = {
336 let current = self.state.current.lock().expect("session mutex poisoned");
337 let session = current
338 .as_ref()
339 .ok_or_else(|| Error::from(HttpError::Unauthorized))?;
340 session.user_id.clone()
341 };
342
343 let target = self
344 .state
345 .service
346 .store()
347 .read(id)
348 .await?
349 .ok_or_else(|| Error::from(HttpError::NotFound))?;
350
351 if target.user_id != current_user_id {
352 return Err(Error::from(HttpError::NotFound));
353 }
354
355 self.state.service.store().destroy(id).await
356 }
357
358 pub async fn list(&self, user_id: &str) -> crate::Result<Vec<Session>> {
362 self.state.service.list(user_id).await
363 }
364
365 pub async fn revoke_by_id(&self, user_id: &str, id: &str) -> crate::Result<()> {
367 self.state.service.revoke(user_id, id).await
368 }
369
370 pub async fn revoke_all(&self, user_id: &str) -> crate::Result<()> {
372 self.state.service.revoke_all(user_id).await
373 }
374
375 pub async fn revoke_all_except(&self, user_id: &str, keep_id: &str) -> crate::Result<()> {
377 self.state.service.revoke_all_except(user_id, keep_id).await
378 }
379}