Skip to main content

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}