async_mongodb_session/
lib.rs

1//! An async-session implementation for MongoDB
2//!
3//! # Examples
4//!
5//! ```
6//! use async_mongodb_session::*;
7//! use async_session::{Session, SessionStore};
8//!
9//! # fn main() -> async_session::Result { async_std::task::block_on(async {
10//! let store = MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection");
11//! # Ok(()) }) }
12//! ```
13
14#![forbid(unsafe_code, future_incompatible, rust_2018_idioms)]
15#![deny(missing_debug_implementations, nonstandard_style)]
16#![warn(missing_docs, rustdoc::missing_doc_code_examples, unreachable_pub)]
17
18use async_session::chrono::{Duration, Utc};
19use async_session::{async_trait, Result, Session, SessionStore};
20use mongodb::bson::{self, doc, Bson, Document};
21use mongodb::options::{ReplaceOptions, SelectionCriteria};
22use mongodb::Client;
23
24/// A MongoDB session store.
25#[derive(Debug, Clone)]
26pub struct MongodbSessionStore {
27    collection: mongodb::Collection<Document>,
28    database: mongodb::Database,
29}
30
31impl MongodbSessionStore {
32    /// Create a new instance of `MongodbSessionStore` after stablish the connection to monngodb.
33    /// ```rust
34    /// # fn main() -> async_session::Result { async_std::task::block_on(async {
35    /// # use async_mongodb_session::MongodbSessionStore;
36    /// let store =
37    /// MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection")
38    /// .await?;
39    /// # Ok(()) }) }
40    /// ```
41    pub async fn new(uri: &str, db_name: &str, coll_name: &str) -> mongodb::error::Result<Self> {
42        let client = Client::with_uri_str(uri).await?;
43        let middleware = Self::from_client(client, db_name, coll_name);
44        middleware.create_expire_index("expireAt", 0).await?;
45        Ok(middleware)
46    }
47
48    /// Create a new instance of `MongodbSessionStore` from an open client.
49    /// ```rust
50    /// use mongodb::{options::ClientOptions, Client};
51    ///
52    /// # fn main() -> async_session::Result { async_std::task::block_on(async {
53    /// # use async_mongodb_session::MongodbSessionStore;
54    ///             let client_options = match ClientOptions::parse("mongodb://127.0.0.1:27017").await {
55    ///     Ok(c) => c,
56    ///     Err(e) => panic!("Client Options Failed: {}", e),
57    /// };
58
59    /// let client = match Client::with_options(client_options) {
60    ///     Ok(c) => c,
61    ///     Err(e) => panic!("Client Creation Failed: {}", e),
62    /// };
63
64    /// let store = MongodbSessionStore::from_client(client, "db_name", "collection");
65    /// # Ok(()) }) }
66    /// ```
67    pub fn from_client(client: Client, db_name: &str, coll_name: &str) -> Self {
68        let database = client.database(db_name);
69        let collection = database.collection(coll_name);
70        Self {
71            database,
72            collection,
73        }
74    }
75
76    /// Initialize the default expiration mechanism, based on the document expiration
77    /// that mongodb provides <https://docs.mongodb.com/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time>.
78    /// The default ttl applyed to sessions without expiry is 20 minutes.
79    /// If the `expireAt` date field contains a date in the past, mongodb considers the document expired and will be deleted.
80    /// Note: mongodb runs the expiration logic every 60 seconds.
81    /// ```rust
82    /// # fn main() -> async_session::Result { async_std::task::block_on(async {
83    /// # use async_mongodb_session::MongodbSessionStore;
84    /// let store =
85    /// MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection")
86    /// .await?;
87    /// store.initialize().await?;
88    /// # Ok(()) }) }
89    /// ```
90    pub async fn initialize(&self) -> Result {
91        self.index_on_expiry_at().await
92    }
93
94    /// private associated function
95    /// Create an `expire after seconds` index in the provided field.
96    /// Testing is covered by initialize test.
97    async fn create_expire_index(
98        &self,
99        field_name: &str,
100        expire_after_seconds: u32,
101    ) -> mongodb::error::Result<()> {
102        let create_index = doc! {
103            "createIndexes": self.collection.name(),
104            "indexes": [
105                {
106                    "key" : { field_name: 1 },
107                    "name": format!("session_expire_index_{}", field_name),
108                    "expireAfterSeconds": expire_after_seconds,
109                }
110            ]
111        };
112        self.database
113            .run_command(
114                create_index,
115                SelectionCriteria::ReadPreference(mongodb::options::ReadPreference::Primary),
116            )
117            .await?;
118        Ok(())
119    }
120
121    /// Create a new index for the `expireAt` property, allowing to expire sessions at a specific clock time.
122    /// If the `expireAt` date field contains a date in the past, mongodb considers the document expired and will be deleted.
123    /// <https://docs.mongodb.com/manual/tutorial/expire-data/#expire-documents-at-a-specific-clock-time>
124    /// ```rust
125    /// # fn main() -> async_session::Result { async_std::task::block_on(async {
126    /// # use async_mongodb_session::MongodbSessionStore;
127    /// let store =
128    /// MongodbSessionStore::new("mongodb://127.0.0.1:27017", "db_name", "collection")
129    /// .await?;
130    /// store.index_on_expiry_at().await?;
131    /// # Ok(()) }) }
132    /// ```
133    pub async fn index_on_expiry_at(&self) -> Result {
134        self.create_expire_index("expireAt", 0).await?;
135        Ok(())
136    }
137}
138
139#[async_trait]
140impl SessionStore for MongodbSessionStore {
141    async fn store_session(&self, session: Session) -> Result<Option<String>> {
142        let coll = &self.collection;
143
144        let value = bson::to_bson(&session)?;
145        let id = session.id();
146        let query = doc! { "session_id": id };
147        let expire_at = match session.expiry() {
148            None => Utc::now() + Duration::from_std(std::time::Duration::from_secs(1200)).unwrap(),
149            Some(expiry) => *{ expiry },
150        };
151        let replacement = doc! { "session_id": id, "session": value, "expireAt": expire_at, "created": Utc::now() };
152
153        let opts = ReplaceOptions::builder().upsert(true).build();
154        coll.replace_one(query, replacement, Some(opts)).await?;
155
156        Ok(session.into_cookie_value())
157    }
158
159    async fn load_session(&self, cookie_value: String) -> Result<Option<Session>> {
160        let id = Session::id_from_cookie_value(&cookie_value)?;
161        let coll = &self.collection;
162        let filter = doc! { "session_id": id };
163        match coll.find_one(filter, None).await? {
164            None => Ok(None),
165            Some(doc) => {
166                let bsession = match doc.get("session") {
167                    Some(v) => v.clone(),
168                    None => return Ok(None),
169                };
170                // mongodb runs the background task that removes expired documents runs every 60 seconds.
171                // https://docs.mongodb.com/manual/core/index-ttl/#timing-of-the-delete-operation
172                // This prevents those documents being returned
173                if let Some(expiry_at) = doc.get("expireAt").and_then(Bson::as_datetime) {
174                    if expiry_at.to_chrono() < Utc::now() {
175                        return Ok(None);
176                    }
177                }
178                Ok(Some(bson::from_bson::<Session>(bsession)?))
179            }
180        }
181    }
182
183    async fn destroy_session(&self, session: Session) -> Result {
184        let coll = &self.collection;
185        coll.delete_one(doc! { "session_id": session.id() }, None)
186            .await?;
187        Ok(())
188    }
189
190    async fn clear_store(&self) -> Result {
191        let coll = &self.collection;
192        coll.drop(None).await?;
193        self.initialize().await?;
194        Ok(())
195    }
196}