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}