cja/server/
session.rs

1use axum::extract::FromRequestParts;
2use http::StatusCode;
3use serde::{Deserialize, Serialize};
4
5use crate::app_state::AppState;
6
7use super::cookies::CookieJar;
8
9/// Core session data that all sessions must contain.
10///
11/// This struct represents the minimal session information stored in the database.
12/// Custom session implementations should include this as a field and add any
13/// additional data needed by the application.
14///
15/// # Example
16///
17/// ```rust
18/// use cja::server::session::CJASession;
19///
20/// #[derive(Debug, Clone)]
21/// struct MySession {
22///     // Required: include the core session data
23///     inner: CJASession,
24///     
25///     // Add custom fields for your application
26///     user_id: Option<i32>,
27///     theme: String,
28///     last_page_visited: Option<String>,
29/// }
30/// ```
31#[derive(Debug, Clone, Deserialize, Serialize, sqlx::FromRow)]
32pub struct CJASession {
33    /// Unique identifier for this session
34    pub session_id: uuid::Uuid,
35    /// Timestamp of last activity (updated on each request)
36    pub updated_at: chrono::DateTime<chrono::Utc>,
37    /// Timestamp when the session was first created
38    pub created_at: chrono::DateTime<chrono::Utc>,
39}
40
41impl CJASession {
42    fn save_cookie<A: AppState>(&self, jar: &CookieJar<A>) {
43        let cookie = tower_cookies::Cookie::build(("session_id", self.session_id.to_string()))
44            .path("/")
45            .http_only(true)
46            .secure(true)
47            .expires(None);
48
49        jar.add(cookie.into());
50    }
51}
52
53// #[async_trait::async_trait]
54// impl<AppState: AS> FromRequestParts<AppState> for Session {
55//     type Rejection = Redirect;
56
57//     async fn from_request_parts(
58//         parts: &mut http::request::Parts,
59//         state: &AppState,
60//     ) -> Result<Self, Self::Rejection> {
61//         let cookies = CookieJar::from_request_parts(parts, state)
62//             .await
63//             .map_err(|_| Redirect::temporary("/login"))?;
64
65//         let SessionCookie(session_id) = SessionCookie::from_request_parts(parts, state)
66//             .await
67//             .map_err(|_| Redirect::temporary("/login"))?;
68
69//         let session_cookie = cookies.get("session_id");
70//         let session = if let Some(session_cookie) = session_cookie {
71//             let session_id = session_cookie.value().to_string();
72//             let Ok(session_id) = uuid::Uuid::parse_str(&session_id) else {
73//                 tracing::error!("Failed to parse session id: {session_id}");
74
75//                 Err(Redirect::temporary("/login"))?
76//             };
77
78//             sqlx::query_as!(
79//                 Session,
80//                 r"
81//         SELECT session_id, updated_at, created_at
82//         FROM Sessions
83//         WHERE session_id = $1
84//         ",
85//                 session_id
86//             )
87//             .fetch_one(state.db())
88//             .await
89//             .map_err(|e| {
90//                 tracing::error!("Failed to fetch session: {e}");
91
92//                 Redirect::temporary("/login")
93//             })?
94//         } else {
95//             Session::create(state, &cookies)
96//                 .await
97//                 .map_err(|_| Redirect::temporary("/login"))?
98//         };
99
100//         Ok(session)
101//     }
102// }
103
104// impl Session {
105//     pub async fn create<AppState: AS>(
106//         app_state: &AppState,
107//         jar: &CookieJar<AppState>,
108//     ) -> color_eyre::Result<Self> {
109//         let session = sqlx::query_as!(
110//             Session,
111//             r"
112//         INSERT INTO Sessions DEFAULT VALUES
113//         RETURNING session_id, updated_at, created_at
114//         ",
115//         )
116//         .fetch_one(app_state.db())
117//         .await?;
118
119//         let session_cookie =
120//             tower_cookies::Cookie::build(("session_id", session.session_id.to_string()))
121//                 .path("/")
122//                 .http_only(true)
123//                 .secure(true)
124//                 .expires(None);
125//         jar.add(session_cookie.into());
126
127//         Ok(session)
128//     }
129// }
130
131/// A trait for implementing custom session types that integrate with the framework's session management.
132///
133/// Sessions are automatically created when needed and persisted across requests using secure cookies.
134/// Your session type must include the `CJASession` as an inner field and can add any additional
135/// fields needed by your application.
136///
137/// # Example
138///
139/// ```rust,no_run
140/// use cja::server::session::{AppSession, CJASession};
141/// use serde::{Serialize, Deserialize};
142///
143/// #[derive(Debug, Clone)]
144/// struct UserSession {
145///     inner: CJASession,
146///     user_id: Option<i32>,
147///     preferences: serde_json::Value,
148/// }
149///
150/// #[async_trait::async_trait]
151/// impl AppSession for UserSession {
152///     async fn from_db(pool: &sqlx::PgPool, session_id: uuid::Uuid) -> cja::Result<Self> {
153///         // Query your session data including any custom fields
154///         // In a real app, you'd have extended the sessions table with these columns
155///         let row = sqlx::query_as::<_, CJASession>(
156///             "SELECT session_id, created_at, updated_at FROM sessions WHERE session_id = $1"
157///         )
158///         .bind(session_id)
159///         .fetch_one(pool)
160///         .await?;
161///         
162///         // For this example, we're just using default values for custom fields
163///         // In a real app, you'd query your extended session data here
164///         Ok(Self {
165///             inner: row,
166///             user_id: None,
167///             preferences: serde_json::json!({}),
168///         })
169///     }
170///     
171///     async fn create(pool: &sqlx::PgPool) -> cja::Result<Self> {
172///         let row = sqlx::query_as::<_, CJASession>(
173///             "INSERT INTO sessions DEFAULT VALUES RETURNING session_id, created_at, updated_at"
174///         )
175///         .fetch_one(pool)
176///         .await?;
177///         
178///         Ok(Self {
179///             inner: row,
180///             user_id: None,
181///             preferences: serde_json::json!({}),
182///         })
183///     }
184///     
185///     fn from_inner(inner: CJASession) -> Self {
186///         Self {
187///             inner,
188///             user_id: None,
189///             preferences: serde_json::json!({}),
190///         }
191///     }
192///     
193///     fn inner(&self) -> &CJASession {
194///         &self.inner
195///     }
196/// }
197/// ```
198///
199/// # Using Sessions in Handlers
200///
201/// ```rust
202/// use axum::response::IntoResponse;
203/// use cja::server::session::Session;
204/// # use cja::server::session::{AppSession, CJASession};
205/// #
206/// # #[derive(Debug, Clone)]
207/// # struct UserSession { inner: CJASession, user_id: Option<i32> }
208/// #
209/// # #[async_trait::async_trait]
210/// # impl AppSession for UserSession {
211/// #     async fn from_db(_: &sqlx::PgPool, _: uuid::Uuid) -> cja::Result<Self> { todo!() }
212/// #     async fn create(_: &sqlx::PgPool) -> cja::Result<Self> { todo!() }
213/// #     fn from_inner(inner: CJASession) -> Self { Self { inner, user_id: None } }
214/// #     fn inner(&self) -> &CJASession { &self.inner }
215/// # }
216///
217/// async fn handler(
218///     Session(session): Session<UserSession>
219/// ) -> impl IntoResponse {
220///     format!("Session ID: {}, User: {:?}",
221///             session.session_id(),
222///             session.user_id)
223/// }
224/// ```
225#[async_trait::async_trait]
226pub trait AppSession: Sized {
227    /// Load a session from the database by its ID.
228    ///
229    /// This method should fetch the session record and any associated data
230    /// from your sessions table.
231    async fn from_db(pool: &sqlx::PgPool, session_id: uuid::Uuid) -> crate::Result<Self>;
232
233    /// Create a new session in the database.
234    ///
235    /// This method should insert a new session record with default values
236    /// and return the created session.
237    async fn create(pool: &sqlx::PgPool) -> crate::Result<Self>;
238
239    /// Create a session instance from the inner `CJASession`.
240    ///
241    /// This is used internally when reconstructing sessions. Custom fields
242    /// should be initialized with default values.
243    fn from_inner(inner: CJASession) -> Self;
244
245    /// Get a reference to the inner `CJASession`.
246    ///
247    /// This provides access to the core session fields like ID and timestamps.
248    fn inner(&self) -> &CJASession;
249
250    fn session_id(&self) -> &uuid::Uuid {
251        &self.inner().session_id
252    }
253
254    fn created_at(&self) -> &chrono::DateTime<chrono::Utc> {
255        &self.inner().created_at
256    }
257
258    fn updated_at(&self) -> &chrono::DateTime<chrono::Utc> {
259        &self.inner().updated_at
260    }
261}
262
263/// An Axum extractor that provides access to the current session.
264///
265/// This extractor automatically handles session creation and loading based on cookies.
266/// If no session exists, a new one is created automatically.
267///
268/// # Example
269///
270/// ```rust
271/// use axum::{Router, routing::get, response::IntoResponse};
272/// use cja::server::session::Session;
273/// # use cja::server::session::{AppSession, CJASession};
274/// #
275/// # #[derive(Debug, Clone)]
276/// # struct UserSession {
277/// #     inner: CJASession,
278/// #     user_id: Option<i32>,
279/// #     login_count: i32,
280/// # }
281/// #
282/// # #[async_trait::async_trait]
283/// # impl AppSession for UserSession {
284/// #     async fn from_db(_: &sqlx::PgPool, _: uuid::Uuid) -> cja::Result<Self> { todo!() }
285/// #     async fn create(_: &sqlx::PgPool) -> cja::Result<Self> { todo!() }
286/// #     fn from_inner(inner: CJASession) -> Self {
287/// #         Self { inner, user_id: None, login_count: 0 }
288/// #     }
289/// #     fn inner(&self) -> &CJASession { &self.inner }
290/// # }
291///
292/// async fn profile_handler(
293///     Session(session): Session<UserSession>
294/// ) -> impl IntoResponse {
295///     match session.user_id {
296///         Some(id) => format!("Welcome back, user {}! Login count: {}", id, session.login_count),
297///         None => "Please log in".to_string(),
298///     }
299/// }
300///
301/// async fn login_handler(
302///     Session(mut session): Session<UserSession>,
303///     // ... other extractors for login data
304/// ) -> impl IntoResponse {
305///     // After successful authentication:
306///     session.user_id = Some(123);
307///     session.login_count += 1;
308///     // Don't forget to save the session!
309///     
310///     "Logged in successfully"
311/// }
312///
313/// # #[derive(Clone)]
314/// # struct MyAppState;
315/// # impl cja::app_state::AppState for MyAppState {
316/// #     fn version(&self) -> &str { "1.0.0" }
317/// #     fn db(&self) -> &sqlx::PgPool { todo!() }
318/// #     fn cookie_key(&self) -> &cja::server::cookies::CookieKey { todo!() }
319/// # }
320/// # fn app() -> Router {
321/// let app = Router::new()
322///     .route("/profile", get(profile_handler))
323///     .route("/login", get(login_handler))
324///     .with_state(MyAppState);
325/// # app
326/// # }
327/// ```
328#[derive(Clone)]
329pub struct Session<A: AppSession>(pub A);
330
331impl<A: AppState + Send + Sync, S: AppSession + Send + Sync> FromRequestParts<A> for Session<S> {
332    type Rejection = StatusCode;
333
334    async fn from_request_parts(
335        parts: &mut http::request::Parts,
336        state: &A,
337    ) -> Result<Self, Self::Rejection> {
338        async fn create_session<A: AppState, S: AppSession>(
339            state: &A,
340            cookies: &CookieJar<A>,
341        ) -> Result<Session<S>, StatusCode> {
342            let session = S::create(state.db())
343                .await
344                .map(Session)
345                .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
346
347            session.0.inner().save_cookie(cookies);
348
349            Ok(session)
350        }
351
352        let cookies = CookieJar::from_request_parts(parts, state)
353            .await
354            .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
355
356        let Some(session_cookie) = cookies.get("session_id") else {
357            return create_session(state, &cookies).await;
358        };
359
360        let session_id = session_cookie.value().to_string();
361        let Ok(session_id) = uuid::Uuid::parse_str(&session_id) else {
362            return create_session(state, &cookies).await;
363        };
364
365        let Ok(session) = S::from_db(state.db(), session_id).await.map(Session) else {
366            return create_session(state, &cookies).await;
367        };
368
369        Ok(session)
370    }
371}