Skip to main content

rainier_session/
manager.rs

1//! [`SessionManager`] — the store and its settings, as one container-storable
2//! value.
3
4use std::sync::Arc;
5
6use chrono::Duration;
7use rainier_http::SameSite;
8use rainier_support::Result;
9
10use crate::session::SessionData;
11use crate::store::SessionStore;
12
13/// How the session cookie is written.
14#[derive(Debug, Clone)]
15pub struct SessionConfig {
16    /// The cookie's name.
17    pub cookie: String,
18    /// Its path.
19    pub path: String,
20    /// Its domain, or `None` for the host that set it.
21    pub domain: Option<String>,
22    /// Whether it is `Secure` — set this in production.
23    pub secure: bool,
24    /// Its `SameSite` policy.
25    pub same_site: SameSite,
26    /// How long a session lives without being touched.
27    pub lifetime: Duration,
28}
29
30impl SessionConfig {
31    /// Name the cookie.
32    pub fn cookie(mut self, name: impl Into<String>) -> Self {
33        self.cookie = name.into();
34        self
35    }
36
37    /// Set the cookie's path.
38    pub fn path(mut self, path: impl Into<String>) -> Self {
39        self.path = path.into();
40        self
41    }
42
43    /// Scope the cookie to a domain.
44    pub fn domain(mut self, domain: impl Into<String>) -> Self {
45        self.domain = Some(domain.into());
46        self
47    }
48
49    /// Send the cookie only over HTTPS.
50    pub fn secure(mut self, secure: bool) -> Self {
51        self.secure = secure;
52        self
53    }
54
55    /// Set the `SameSite` policy.
56    pub fn same_site(mut self, same_site: SameSite) -> Self {
57        self.same_site = same_site;
58        self
59    }
60
61    /// How long a session lives.
62    pub fn lifetime(mut self, lifetime: Duration) -> Self {
63        self.lifetime = lifetime;
64        self
65    }
66}
67
68/// The application's session store and settings.
69///
70/// This is what the container holds and what the `Session` facade resolves.
71///
72/// **It is not a request's session.** A facade is process-global and a session
73/// belongs to one request, so there is nothing honest for
74/// `Session::instance().get("user_id")` to return. Per-request access is
75/// [`request.session()`](crate::SessionRequestExt::session); this type is for
76/// the operations that genuinely are application-wide — reading or destroying
77/// a session by id, and collecting expired ones.
78#[derive(Clone)]
79pub struct SessionManager {
80    store: Arc<dyn SessionStore>,
81    config: SessionConfig,
82}
83
84impl SessionManager {
85    /// A manager over `store`, with the default cookie settings.
86    pub fn new(store: Arc<dyn SessionStore>) -> Self {
87        Self { store, config: SessionConfig::default() }
88    }
89
90    /// A manager with explicit settings.
91    pub fn with_config(store: Arc<dyn SessionStore>, config: SessionConfig) -> Self {
92        Self { store, config }
93    }
94
95    /// Adjust the settings.
96    pub fn configure(mut self, adjust: impl FnOnce(SessionConfig) -> SessionConfig) -> Self {
97        self.config = adjust(self.config);
98        self
99    }
100
101    /// The store.
102    pub fn store(&self) -> &Arc<dyn SessionStore> {
103        &self.store
104    }
105
106    /// The cookie settings.
107    pub fn config(&self) -> &SessionConfig {
108        &self.config
109    }
110
111    /// The driver's name — `"memory"`, `"database"`.
112    pub fn driver(&self) -> &str {
113        self.store.name()
114    }
115
116    /// Read a session by id, without a request.
117    ///
118    /// For an administrative view: "what is in this user's session".
119    pub async fn read(&self, id: &str) -> Result<Option<SessionData>> {
120        self.store.read(id).await
121    }
122
123    /// Destroy a session by id.
124    ///
125    /// The other half of "log this device out", and the thing to call for
126    /// every one of a user's sessions when their password changes.
127    pub async fn destroy(&self, id: &str) -> Result<()> {
128        self.store.destroy(id).await
129    }
130
131    /// Discard every expired session. Returns how many.
132    pub async fn gc(&self) -> Result<u64> {
133        self.store.gc().await
134    }
135}
136
137impl std::fmt::Debug for SessionManager {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        f.debug_struct("SessionManager")
140            .field("driver", &self.store.name())
141            .field("cookie", &self.config.cookie)
142            .field("lifetime", &self.config.lifetime)
143            .finish()
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::store::MemorySessionStore;
151
152    fn manager() -> SessionManager {
153        SessionManager::new(Arc::new(MemorySessionStore::default()))
154    }
155
156    #[test]
157    fn the_defaults_are_the_safe_ones() {
158        let config = SessionConfig::default();
159
160        assert_eq!(config.same_site, SameSite::Lax);
161        assert_eq!(config.path, "/");
162        assert!(!config.secure, "off by default so http://localhost works");
163    }
164
165    #[test]
166    fn settings_are_configurable() {
167        let manager = manager().configure(|config| {
168            config
169                .cookie("my_app_session")
170                .secure(true)
171                .same_site(SameSite::Strict)
172                .lifetime(Duration::days(14))
173                .domain("example.com")
174        });
175
176        assert_eq!(manager.config().cookie, "my_app_session");
177        assert!(manager.config().secure);
178        assert_eq!(manager.config().same_site, SameSite::Strict);
179        assert_eq!(manager.config().lifetime, Duration::days(14));
180        assert_eq!(manager.config().domain.as_deref(), Some("example.com"));
181    }
182
183    #[tokio::test]
184    async fn a_session_can_be_read_and_destroyed_without_a_request() {
185        let manager = manager();
186        let mut values = serde_json::Map::new();
187        values.insert("user_id".into(), 42.into());
188        let data = SessionData { values, flash: Vec::new() };
189
190        manager.store().write("abc", &data).await.unwrap();
191        assert!(manager.read("abc").await.unwrap().is_some());
192
193        manager.destroy("abc").await.unwrap();
194        assert!(manager.read("abc").await.unwrap().is_none());
195    }
196
197    #[test]
198    fn the_driver_is_reported() {
199        assert_eq!(manager().driver(), "memory");
200    }
201}