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}