reinhardt_middleware/session/injectable.rs
1//! `Injectable` implementation exposing `SessionData` to the DI layer.
2//!
3//! The session store is contributed to the DI container by
4//! [`SessionMiddleware::di_registrations`] both under
5//! `TypeId::of::<SessionStore>()` for direct session loading and under
6//! `FactoryOutput<SessionStoreKey, Arc<SessionStore>>` for handlers that use
7//! `#[inject] store: Depends<SessionStoreKey, Arc<SessionStore>>`.
8//! This module's `Injectable for SessionData` impl reaches into the raw
9//! singleton entry by `TypeId` to load the per-request session.
10
11use async_trait::async_trait;
12use reinhardt_di::{DiError, DiResult, Injectable, InjectionContext};
13use reinhardt_http::Request;
14
15use super::cookie::find_cookie_value;
16use super::data::SessionData;
17use super::id::{ActiveSessionId, SessionCookieName, SessionId};
18use super::store::SessionStore;
19
20/// Default session cookie name used when no `SessionCookieName` extension is present.
21const DEFAULT_SESSION_COOKIE_NAME: &str = "sessionid";
22
23/// Helper function to extract session ID from HTTP request cookies.
24///
25/// Searches for a cookie with the specified name in the Cookie header.
26///
27/// # Arguments
28///
29/// * `request` - The HTTP request to extract the session ID from
30/// * `cookie_name` - The name of the session cookie (e.g., "sessionid")
31///
32/// # Returns
33///
34/// * `Ok(String)` - The session ID if found and valid
35/// * `Err(DiError)` - If the cookie header is missing, invalid, or the session cookie is not found
36fn extract_session_id_from_request(request: &Request, cookie_name: &str) -> DiResult<String> {
37 find_cookie_value(request, cookie_name).ok_or_else(|| {
38 DiError::NotFound(format!(
39 "Session cookie '{}' not found in Cookie header",
40 cookie_name
41 ))
42 })
43}
44
45#[async_trait]
46impl Injectable for SessionData {
47 async fn inject(ctx: &InjectionContext) -> DiResult<Self> {
48 // Look up the raw store under TypeId::of::<SessionStore>() — the same
49 // singleton entry SessionMiddleware::di_registrations keeps for
50 // SessionData loading. Handler code should use the keyed
51 // FactoryOutput<SessionStoreKey, Arc<SessionStore>> registration.
52 let store = ctx.get_singleton::<SessionStore>().ok_or_else(|| {
53 DiError::NotFound(
54 concat!(
55 "SessionStore not found in SingletonScope. ",
56 "Ensure SessionMiddleware is configured and its store is registered."
57 )
58 .to_string(),
59 )
60 })?;
61
62 // Get Request from context
63 let request = ctx.get_request::<Request>().ok_or_else(|| {
64 DiError::NotFound("Request not found in InjectionContext".to_string())
65 })?;
66
67 // Extract configured cookie name from request extensions.
68 // Extensions::get returns an owned value, so we extract it once and
69 // use a reference for the lookup to avoid additional allocation.
70 let ext_cookie_name = request.extensions.get::<SessionCookieName>();
71 let cookie_name = ext_cookie_name
72 .as_ref()
73 .map(|cn| cn.as_str())
74 .unwrap_or(DEFAULT_SESSION_COOKIE_NAME);
75
76 // Prefer the SessionId injected by SessionMiddleware (present for all requests,
77 // including those without a Cookie header such as the initial login request).
78 // Fall back to parsing the Cookie header for requests that bypass the middleware.
79 let session_id = if let Some(sid) = request.extensions.get::<SessionId>() {
80 sid.as_ref().to_string()
81 } else {
82 extract_session_id_from_request(&request, cookie_name)?
83 };
84
85 // Load SessionData from store, attaching the request-scoped active session
86 // ID holder so `SessionData::regenerate_id` can keep the middleware's
87 // `Set-Cookie` value in sync with rotations. See #3827.
88 let id_holder = request.extensions.get::<ActiveSessionId>();
89 let mut session = store
90 .get(&session_id)
91 .filter(|s| s.is_valid())
92 .ok_or_else(|| {
93 DiError::NotFound("Valid session not found. Session may have expired.".to_string())
94 })?;
95 session.id_holder = id_holder;
96 Ok(session)
97 }
98}