Skip to main content

reinhardt_middleware/session/
auth_ext.rs

1//! [`SessionAuthExt`]: ergonomic login/logout helpers on [`SessionData`].
2//!
3//! Collapses the four-step "rotate id → write user id → delete old store
4//! entry → save new store entry" sequence that every server-function login
5//! handler has had to spell out by hand. Symmetrically wraps the logout
6//! sequence so callers cannot forget to rotate the id before clearing the
7//! user reference. See issue #4446.
8
9use reinhardt_http::Result;
10use serde::Serialize;
11
12use super::data::{SessionData, USER_ID_SESSION_KEY};
13use super::store::SessionStore;
14
15/// Login/logout helpers for [`SessionData`].
16///
17/// Both methods perform the session-fixation prevention rotation that is a
18/// required step on authentication state transitions: each call regenerates
19/// the session id, removes the old store entry referenced by the previous
20/// id, and persists the updated [`SessionData`] under the new id.
21///
22/// The trait is provided as an extension so existing call sites can opt
23/// in by adding a single `use` and replacing their inline blocks; the
24/// implementation lives in `reinhardt-middleware` because that is the
25/// crate that owns [`SessionData`] and [`SessionStore`]. `BaseUser` is
26/// deliberately *not* a bound on `login` — taking `impl Serialize` keeps
27/// the helper usable with any primary-key shape (`i64`, `Uuid`, a tenant
28/// composite key, …) and avoids the otherwise-circular auth ↔ middleware
29/// coupling.
30///
31/// The `store` parameter is a `&SessionStore`. Callers that have
32/// `#[inject] store: Depends<SessionStoreKey, Arc<SessionStore>>` can pass
33/// `&**store`, dereferencing first through [`Depends`][reinhardt_di::Depends]
34/// to the shared `Arc<SessionStore>` and then through `Arc` to `SessionStore`.
35///
36/// # Usage
37///
38/// ```rust,ignore
39/// use reinhardt::di::Depends;
40/// use reinhardt::middleware::session::{
41///     SessionAuthExt, SessionData, SessionStore, SessionStoreKey,
42/// };
43/// use std::sync::Arc;
44///
45/// #[server_fn]
46/// pub async fn login(
47///     username: String,
48///     password: String,
49///     #[inject] mut session: SessionData,
50///     #[inject] store: Depends<SessionStoreKey, Arc<SessionStore>>,
51/// ) -> Result<(), ServerFnError> {
52///     // … authenticate `user` …
53///     session.login(&**store, user.id())
54///         .map_err(|e| ServerFnError::application(e.to_string()))?;
55///     Ok(())
56/// }
57/// ```
58pub trait SessionAuthExt {
59	/// Mark the current session as authenticated for `user_id`.
60	///
61	/// Equivalent to the inline sequence:
62	///
63	/// ```text
64	/// let old_id = self.regenerate_id();
65	/// self.set(USER_ID_SESSION_KEY.to_string(), user_id)?;
66	/// store.delete(&old_id);
67	/// store.save(self.clone());
68	/// ```
69	///
70	/// Returns a [`reinhardt_http::Result`] so the serialisation failure
71	/// inside [`SessionData::set`] propagates with the same error type as
72	/// the rest of the session API.
73	fn login<V: Serialize + Send + Sync>(&mut self, store: &SessionStore, user_id: V)
74	-> Result<()>;
75
76	/// Clear the authenticated-user reference from the current session.
77	///
78	/// Rotates the session id, removes the old store entry, drops the
79	/// user-id key from the session map (without clearing any other
80	/// keys callers may have written), and persists the rotated session.
81	/// Callers who want to drop *all* session state should call
82	/// [`SessionData::clear`] before invoking this helper.
83	fn logout(&mut self, store: &SessionStore);
84}
85
86impl SessionAuthExt for SessionData {
87	fn login<V: Serialize + Send + Sync>(
88		&mut self,
89		store: &SessionStore,
90		user_id: V,
91	) -> Result<()> {
92		let old_id = self.regenerate_id();
93		self.set(USER_ID_SESSION_KEY.to_string(), user_id)?;
94		store.delete(&old_id);
95		store.save(self.clone());
96		Ok(())
97	}
98
99	fn logout(&mut self, store: &SessionStore) {
100		let old_id = self.regenerate_id();
101		self.delete(USER_ID_SESSION_KEY);
102		store.delete(&old_id);
103		store.save(self.clone());
104	}
105}