acton_htmx/extractors/
session.rs

1//! Session and flash message extractors
2//!
3//! Provides axum extractors for accessing session data and flash messages
4//! within request handlers.
5//!
6//! The session data is placed in request extensions by `SessionMiddleware`.
7//! Flash messages can be consumed (cleared after read) via `FlashExtractor`.
8
9use crate::auth::session::{FlashMessage, SessionData, SessionId};
10use axum::{
11    extract::FromRequestParts,
12    http::{request::Parts, StatusCode},
13};
14use std::convert::Infallible;
15
16/// Extractor for session data
17///
18/// Extracts the current session from request extensions.
19/// Requires `SessionMiddleware` to be applied to the router.
20///
21/// # Example
22///
23/// ```rust,ignore
24/// use acton_htmx::extractors::SessionExtractor;
25///
26/// async fn handler(SessionExtractor(session_id, session): SessionExtractor) {
27///     if let Some(user_id) = session.user_id {
28///         // User is authenticated
29///     }
30/// }
31/// ```
32#[derive(Debug, Clone)]
33pub struct SessionExtractor(pub SessionId, pub SessionData);
34
35impl<S> FromRequestParts<S> for SessionExtractor
36where
37    S: Send + Sync,
38{
39    type Rejection = (StatusCode, &'static str);
40
41    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
42        let session_id = parts
43            .extensions
44            .get::<SessionId>()
45            .cloned()
46            .ok_or((StatusCode::INTERNAL_SERVER_ERROR, "Session not initialized"))?;
47
48        let session_data = parts
49            .extensions
50            .get::<SessionData>()
51            .cloned()
52            .ok_or((StatusCode::INTERNAL_SERVER_ERROR, "Session data not found"))?;
53
54        Ok(Self(session_id, session_data))
55    }
56}
57
58/// Extractor for flash messages
59///
60/// Extracts flash messages from the session and clears them from the session data.
61/// Messages are typically shown once and then cleared (flash = one-time display).
62///
63/// # Note
64///
65/// This extractor takes the flash messages from the session data in extensions,
66/// clearing them so they won't be persisted back. The middleware will save the
67/// modified session data (without the flashes) on response.
68///
69/// # Example
70///
71/// ```rust,ignore
72/// use acton_htmx::extractors::FlashExtractor;
73///
74/// async fn handler(FlashExtractor(messages): FlashExtractor) {
75///     for msg in messages {
76///         println!("Flash: {:?} - {}", msg.level, msg.message);
77///     }
78/// }
79/// ```
80#[derive(Debug, Clone, Default)]
81pub struct FlashExtractor(pub Vec<FlashMessage>);
82
83impl<S> FromRequestParts<S> for FlashExtractor
84where
85    S: Send + Sync,
86{
87    type Rejection = Infallible;
88
89    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
90        // Take flash messages from session data in extensions (clears them)
91        let messages = parts
92            .extensions
93            .get_mut::<SessionData>()
94            .map(|session| std::mem::take(&mut session.flash_messages))
95            .unwrap_or_default();
96
97        Ok(Self(messages))
98    }
99}
100
101/// Optional session extractor
102///
103/// Returns `None` if session is not available, rather than failing.
104/// Useful for routes that can work with or without a session.
105///
106/// # Example
107///
108/// ```rust,ignore
109/// use acton_htmx::extractors::OptionalSession;
110///
111/// async fn handler(OptionalSession(session): OptionalSession) {
112///     match session {
113///         Some((id, data)) => { /* Authenticated */ }
114///         None => { /* Anonymous */ }
115///     }
116/// }
117/// ```
118#[derive(Debug, Clone)]
119pub struct OptionalSession(pub Option<(SessionId, SessionData)>);
120
121impl<S> FromRequestParts<S> for OptionalSession
122where
123    S: Send + Sync,
124{
125    type Rejection = Infallible;
126
127    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
128        let session = parts
129            .extensions
130            .get::<SessionId>()
131            .cloned()
132            .and_then(|id| {
133                parts
134                    .extensions
135                    .get::<SessionData>()
136                    .cloned()
137                    .map(|data| (id, data))
138            });
139
140        Ok(Self(session))
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn test_flash_extractor_default() {
150        let flash = FlashExtractor::default();
151        assert!(flash.0.is_empty());
152    }
153
154    #[test]
155    fn test_optional_session_default() {
156        // Just verify the types compile correctly
157        let session: OptionalSession = OptionalSession(None);
158        assert!(session.0.is_none());
159    }
160}