reinhardt_middleware/session/data.rs
1//! `SessionData`: per-session payload + helpers for read/write/rotate.
2
3use reinhardt_http::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::time::{Duration, SystemTime};
7use uuid::Uuid;
8
9use super::id::ActiveSessionId;
10
11/// Canonical session-store key used by Reinhardt examples to persist the
12/// authenticated user's primary key after a successful login.
13///
14/// This is the key consumed by the [`crate::session::SessionValue`] and
15/// [`crate::session::OptionalSessionValue`] extractors and written by the
16/// [`crate::session::SessionAuthExt`] helper trait. Application code should
17/// reference this constant instead of hardcoding `"user_id"` so that any
18/// future migration to a different key (for example, the Django-compatible
19/// `_auth_user_id` used by `reinhardt-auth::session`) is mechanical.
20///
21/// See issue #4446.
22pub const USER_ID_SESSION_KEY: &str = "user_id";
23
24/// Session data
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[non_exhaustive]
27pub struct SessionData {
28 /// Session ID
29 pub id: String,
30 /// Data
31 pub data: HashMap<String, serde_json::Value>,
32 /// Creation timestamp
33 pub created_at: SystemTime,
34 /// Last access timestamp
35 pub last_accessed: SystemTime,
36 /// Expiration timestamp
37 pub expires_at: SystemTime,
38 /// Back-reference to the request-scoped active session ID holder.
39 ///
40 /// Populated by `SessionData::inject` from the request extensions; used by
41 /// `regenerate_id` to keep the middleware's `Set-Cookie` value in sync
42 /// with the rotated session ID. Never serialized — sessions persisted to a
43 /// store carry only the data they own. See #3827.
44 ///
45 /// Defaults to `None`; callers constructing `SessionData` literally outside
46 /// the middleware (tests, fixtures) can leave it `None` because rotation
47 /// only matters when the session is actively wired into a live request.
48 #[serde(skip)]
49 pub id_holder: Option<ActiveSessionId>,
50}
51
52impl SessionData {
53 /// Create a new session
54 pub fn new(ttl: Duration) -> Self {
55 let now = SystemTime::now();
56 Self {
57 id: Uuid::new_v4().to_string(),
58 data: HashMap::new(),
59 created_at: now,
60 last_accessed: now,
61 expires_at: now + ttl,
62 id_holder: None,
63 }
64 }
65
66 /// Rotate the session ID (e.g., after authentication, to prevent session
67 /// fixation). Updates both `self.id` and the request-scoped
68 /// [`ActiveSessionId`] so that `SessionMiddleware` writes the new ID to
69 /// the response cookie.
70 ///
71 /// Returns the previous ID so callers can delete the stale entry from
72 /// the store.
73 ///
74 /// See #3827.
75 pub fn regenerate_id(&mut self) -> String {
76 let old_id = std::mem::replace(&mut self.id, Uuid::now_v7().to_string());
77 if let Some(holder) = &self.id_holder {
78 holder.set(self.id.clone());
79 }
80 old_id
81 }
82
83 /// Check if session is valid
84 pub(super) fn is_valid(&self) -> bool {
85 SystemTime::now() < self.expires_at
86 }
87
88 /// Update last access timestamp
89 pub fn touch(&mut self, ttl: Duration) {
90 let now = SystemTime::now();
91 self.last_accessed = now;
92 self.expires_at = now + ttl;
93 }
94
95 /// Get a value
96 pub fn get<T>(&self, key: &str) -> Option<T>
97 where
98 T: for<'de> Deserialize<'de>,
99 {
100 self.data
101 .get(key)
102 .and_then(|v| serde_json::from_value(v.clone()).ok())
103 }
104
105 /// Set a value
106 pub fn set<T>(&mut self, key: String, value: T) -> Result<()>
107 where
108 T: Serialize,
109 {
110 self.data.insert(
111 key,
112 serde_json::to_value(value)
113 .map_err(|e| reinhardt_core::exception::Error::Serialization(e.to_string()))?,
114 );
115 Ok(())
116 }
117
118 /// Delete a value
119 pub fn delete(&mut self, key: &str) {
120 self.data.remove(key);
121 }
122
123 /// Check if a key exists
124 pub fn contains_key(&self, key: &str) -> bool {
125 self.data.contains_key(key)
126 }
127
128 /// Clear the session
129 pub fn clear(&mut self) {
130 self.data.clear();
131 }
132}