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}