actix_session/lib.rs
1//! Session management for Actix Web.
2//!
3//! The HTTP protocol, at a first glance, is stateless: the client sends a request, the server
4//! parses its content, performs some processing and returns a response. The outcome is only
5//! influenced by the provided inputs (i.e. the request content) and whatever state the server
6//! queries while performing its processing.
7//!
8//! Stateless systems are easier to reason about, but they are not quite as powerful as we need them
9//! to be - e.g. how do you authenticate a user? The user would be forced to authenticate **for
10//! every single request**. That is, for example, how 'Basic' Authentication works. While it may
11//! work for a machine user (i.e. an API client), it is impractical for a person—you do not want a
12//! login prompt on every single page you navigate to!
13//!
14//! There is a solution - **sessions**. Using sessions the server can attach state to a set of
15//! requests coming from the same client. They are built on top of cookies - the server sets a
16//! cookie in the HTTP response (`Set-Cookie` header), the client (e.g. the browser) will store the
17//! cookie and play it back to the server when sending new requests (using the `Cookie` header).
18//!
19//! We refer to the cookie used for sessions as a **session cookie**. Its content is called
20//! **session key** (or **session ID**), while the state attached to the session is referred to as
21//! **session state**.
22//!
23//! `actix-session` provides an easy-to-use framework to manage sessions in applications built on
24//! top of Actix Web. [`SessionMiddleware`] is the middleware underpinning the functionality
25//! provided by `actix-session`; it takes care of all the session cookie handling and instructs the
26//! **storage backend** to create/delete/update the session state based on the operations performed
27//! against the active [`Session`].
28//!
29//! `actix-session` provides some built-in storage backends: ([`CookieSessionStore`],
30//! [`RedisSessionStore`]) - you can create a custom storage backend by implementing the
31//! [`SessionStore`] trait.
32//!
33//! Further reading on sessions:
34//! - [RFC 6265](https://datatracker.ietf.org/doc/html/rfc6265);
35//! - [OWASP's session management cheat-sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html).
36//!
37//! # Getting started
38//! To start using sessions in your Actix Web application you must register [`SessionMiddleware`]
39//! as a middleware on your `App`:
40//!
41//! ```no_run
42//! use actix_web::{web, App, HttpServer, HttpResponse, Error};
43//! use actix_session::{Session, SessionMiddleware, storage::RedisSessionStore};
44//! use actix_web::cookie::Key;
45//!
46//! #[actix_web::main]
47//! async fn main() -> std::io::Result<()> {
48//! // When using `Key::generate()` it is important to initialize outside of the
49//! // `HttpServer::new` closure. When deployed the secret key should be read from a
50//! // configuration file or environment variables.
51//! let secret_key = Key::generate();
52//!
53//! let redis_store = RedisSessionStore::new("redis://127.0.0.1:6379")
54//! .await
55//! .unwrap();
56//!
57//! HttpServer::new(move ||
58//! App::new()
59//! // Add session management to your application using Redis for session state storage
60//! .wrap(
61//! SessionMiddleware::new(
62//! redis_store.clone(),
63//! secret_key.clone(),
64//! )
65//! )
66//! .default_service(web::to(|| HttpResponse::Ok())))
67//! .bind(("127.0.0.1", 8080))?
68//! .run()
69//! .await
70//! }
71//! ```
72//!
73//! The session state can be accessed and modified by your request handlers using the [`Session`]
74//! extractor. Note that this doesn't work in the stream of a streaming response.
75//!
76//! ```no_run
77//! use actix_web::Error;
78//! use actix_session::Session;
79//!
80//! fn index(session: Session) -> Result<&'static str, Error> {
81//! // access the session state
82//! if let Some(count) = session.get::<i32>("counter")? {
83//! println!("SESSION value: {}", count);
84//! // modify the session state
85//! session.insert("counter", count + 1)?;
86//! } else {
87//! session.insert("counter", 1)?;
88//! }
89//!
90//! Ok("Welcome!")
91//! }
92//! ```
93//!
94//! # Choosing A Backend
95//!
96//! By default, `actix-session` does not provide any storage backend to retrieve and save the state
97//! attached to your sessions. You can enable:
98//!
99//! - a purely cookie-based "backend", [`CookieSessionStore`], using the `cookie-session` feature
100//! flag.
101//!
102//! ```console
103//! cargo add actix-session --features=cookie-session
104//! ```
105//!
106//! - a Redis-based backend via the [`redis`] crate, [`RedisSessionStore`], using the
107//! `redis-session` feature flag.
108//!
109//! ```console
110//! cargo add actix-session --features=redis-session
111//! ```
112//!
113//! Add the `redis-session-native-tls` feature flag if you want to connect to Redis using a secure
114//! connection (via the `native-tls` crate):
115//!
116//! ```console
117//! cargo add actix-session --features=redis-session-native-tls
118//! ```
119//!
120//! If you, instead, prefer depending on `rustls`, use the `redis-session-rustls` feature flag:
121//!
122//! ```console
123//! cargo add actix-session --features=redis-session-rustls
124//! ```
125//!
126//! You can implement your own session storage backend using the [`SessionStore`] trait.
127//!
128//! [`SessionStore`]: storage::SessionStore
129//! [`CookieSessionStore`]: storage::CookieSessionStore
130//! [`RedisSessionStore`]: storage::RedisSessionStore
131
132#![forbid(unsafe_code)]
133#![warn(missing_docs)]
134#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
135#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
136#![cfg_attr(docsrs, feature(doc_auto_cfg))]
137
138pub mod config;
139mod middleware;
140mod session;
141mod session_ext;
142pub mod storage;
143
144pub use self::{
145 middleware::SessionMiddleware,
146 session::{Session, SessionGetError, SessionInsertError, SessionStatus},
147 session_ext::SessionExt,
148};
149
150#[cfg(test)]
151#[allow(missing_docs)]
152pub mod test_helpers {
153 use actix_web::cookie::Key;
154
155 use crate::{config::CookieContentSecurity, storage::SessionStore};
156
157 /// Generate a random cookie signing/encryption key.
158 pub fn key() -> Key {
159 Key::generate()
160 }
161
162 /// A ready-to-go acceptance test suite to verify that sessions behave as expected
163 /// regardless of the underlying session store.
164 ///
165 /// `is_invalidation_supported` must be set to `true` if the backend supports
166 /// "remembering" that a session has been invalidated (e.g. by logging out).
167 /// It should be to `false` if the backend allows multiple cookies to be active
168 /// at the same time (e.g. cookie store backend).
169 pub async fn acceptance_test_suite<F, Store>(store_builder: F, is_invalidation_supported: bool)
170 where
171 Store: SessionStore + 'static,
172 F: Fn() -> Store + Clone + Send + 'static,
173 {
174 for policy in &[
175 CookieContentSecurity::Signed,
176 CookieContentSecurity::Private,
177 ] {
178 println!("Using {policy:?} as cookie content security policy.");
179 acceptance_tests::basic_workflow(store_builder.clone(), *policy).await;
180 acceptance_tests::expiration_is_refreshed_on_changes(store_builder.clone(), *policy)
181 .await;
182 acceptance_tests::expiration_is_always_refreshed_if_configured_to_refresh_on_every_request(
183 store_builder.clone(),
184 *policy,
185 )
186 .await;
187 acceptance_tests::complex_workflow(
188 store_builder.clone(),
189 is_invalidation_supported,
190 *policy,
191 )
192 .await;
193 acceptance_tests::guard(store_builder.clone(), *policy).await;
194 }
195 }
196
197 mod acceptance_tests {
198 use actix_web::{
199 cookie::time,
200 dev::{Service, ServiceResponse},
201 guard, middleware, test,
202 web::{self, get, post, resource, Bytes},
203 App, HttpResponse, Result,
204 };
205 use serde::{Deserialize, Serialize};
206 use serde_json::json;
207
208 use crate::{
209 config::{CookieContentSecurity, PersistentSession, TtlExtensionPolicy},
210 storage::SessionStore,
211 test_helpers::key,
212 Session, SessionExt, SessionMiddleware,
213 };
214
215 pub(super) async fn basic_workflow<F, Store>(
216 store_builder: F,
217 policy: CookieContentSecurity,
218 ) where
219 Store: SessionStore + 'static,
220 F: Fn() -> Store + Clone + Send + 'static,
221 {
222 let app = test::init_service(
223 App::new()
224 .wrap(
225 SessionMiddleware::builder(store_builder(), key())
226 .cookie_path("/test/".into())
227 .cookie_name("actix-test".into())
228 .cookie_domain(Some("localhost".into()))
229 .cookie_content_security(policy)
230 .session_lifecycle(
231 PersistentSession::default()
232 .session_ttl(time::Duration::seconds(100)),
233 )
234 .build(),
235 )
236 .service(web::resource("/").to(|ses: Session| async move {
237 let _ = ses.insert("counter", 100);
238 "test"
239 }))
240 .service(web::resource("/test/").to(|ses: Session| async move {
241 let val: usize = ses.get("counter").unwrap().unwrap();
242 format!("counter: {val}")
243 })),
244 )
245 .await;
246
247 let request = test::TestRequest::get().to_request();
248 let response = app.call(request).await.unwrap();
249 let cookie = response.get_cookie("actix-test").unwrap().clone();
250 assert_eq!(cookie.path().unwrap(), "/test/");
251
252 let request = test::TestRequest::with_uri("/test/")
253 .cookie(cookie)
254 .to_request();
255 let body = test::call_and_read_body(&app, request).await;
256 assert_eq!(body, Bytes::from_static(b"counter: 100"));
257 }
258
259 pub(super) async fn expiration_is_always_refreshed_if_configured_to_refresh_on_every_request<
260 F,
261 Store,
262 >(
263 store_builder: F,
264 policy: CookieContentSecurity,
265 ) where
266 Store: SessionStore + 'static,
267 F: Fn() -> Store + Clone + Send + 'static,
268 {
269 let session_ttl = time::Duration::seconds(60);
270 let app = test::init_service(
271 App::new()
272 .wrap(
273 SessionMiddleware::builder(store_builder(), key())
274 .cookie_content_security(policy)
275 .session_lifecycle(
276 PersistentSession::default()
277 .session_ttl(session_ttl)
278 .session_ttl_extension_policy(
279 TtlExtensionPolicy::OnEveryRequest,
280 ),
281 )
282 .build(),
283 )
284 .service(web::resource("/").to(|ses: Session| async move {
285 let _ = ses.insert("counter", 100);
286 "test"
287 }))
288 .service(web::resource("/test/").to(|| async move { "no-changes-in-session" })),
289 )
290 .await;
291
292 // Create session
293 let request = test::TestRequest::get().to_request();
294 let response = app.call(request).await.unwrap();
295 let cookie_1 = response.get_cookie("id").expect("Cookie is set");
296 assert_eq!(cookie_1.max_age(), Some(session_ttl));
297
298 // Fire a request that doesn't touch the session state, check
299 // that the session cookie is present and its expiry is set to the maximum we configured.
300 let request = test::TestRequest::with_uri("/test/")
301 .cookie(cookie_1)
302 .to_request();
303 let response = app.call(request).await.unwrap();
304 let cookie_2 = response.get_cookie("id").expect("Cookie is set");
305 assert_eq!(cookie_2.max_age(), Some(session_ttl));
306 }
307
308 pub(super) async fn expiration_is_refreshed_on_changes<F, Store>(
309 store_builder: F,
310 policy: CookieContentSecurity,
311 ) where
312 Store: SessionStore + 'static,
313 F: Fn() -> Store + Clone + Send + 'static,
314 {
315 let session_ttl = time::Duration::seconds(60);
316 let app = test::init_service(
317 App::new()
318 .wrap(
319 SessionMiddleware::builder(store_builder(), key())
320 .cookie_content_security(policy)
321 .session_lifecycle(
322 PersistentSession::default().session_ttl(session_ttl),
323 )
324 .build(),
325 )
326 .service(web::resource("/").to(|ses: Session| async move {
327 let _ = ses.insert("counter", 100);
328 "test"
329 }))
330 .service(web::resource("/test/").to(|| async move { "no-changes-in-session" })),
331 )
332 .await;
333
334 let request = test::TestRequest::get().to_request();
335 let response = app.call(request).await.unwrap();
336 let cookie_1 = response.get_cookie("id").expect("Cookie is set");
337 assert_eq!(cookie_1.max_age(), Some(session_ttl));
338
339 let request = test::TestRequest::with_uri("/test/")
340 .cookie(cookie_1.clone())
341 .to_request();
342 let response = app.call(request).await.unwrap();
343 assert!(response.response().cookies().next().is_none());
344
345 let request = test::TestRequest::get().cookie(cookie_1).to_request();
346 let response = app.call(request).await.unwrap();
347 let cookie_2 = response.get_cookie("id").expect("Cookie is set");
348 assert_eq!(cookie_2.max_age(), Some(session_ttl));
349 }
350
351 pub(super) async fn guard<F, Store>(store_builder: F, policy: CookieContentSecurity)
352 where
353 Store: SessionStore + 'static,
354 F: Fn() -> Store + Clone + Send + 'static,
355 {
356 let srv = actix_test::start(move || {
357 App::new()
358 .wrap(
359 SessionMiddleware::builder(store_builder(), key())
360 .cookie_name("test-session".into())
361 .cookie_content_security(policy)
362 .session_lifecycle(
363 PersistentSession::default().session_ttl(time::Duration::days(7)),
364 )
365 .build(),
366 )
367 .wrap(middleware::Logger::default())
368 .service(resource("/").route(get().to(index)))
369 .service(resource("/do_something").route(post().to(do_something)))
370 .service(resource("/login").route(post().to(login)))
371 .service(resource("/logout").route(post().to(logout)))
372 .service(
373 web::scope("/protected")
374 .guard(guard::fn_guard(|g| {
375 g.get_session().get::<String>("user_id").unwrap().is_some()
376 }))
377 .service(resource("/count").route(get().to(count))),
378 )
379 });
380
381 // Step 1: GET without session info
382 // - response should be a unsuccessful status
383 let req_1 = srv.get("/protected/count").send();
384 let resp_1 = req_1.await.unwrap();
385 assert!(!resp_1.status().is_success());
386
387 // Step 2: POST to login
388 // - set-cookie actix-session will be in response (session cookie #1)
389 // - updates session state: {"counter": 0, "user_id": "ferris"}
390 let req_2 = srv.post("/login").send_json(&json!({"user_id": "ferris"}));
391 let resp_2 = req_2.await.unwrap();
392 let cookie_1 = resp_2
393 .cookies()
394 .unwrap()
395 .clone()
396 .into_iter()
397 .find(|c| c.name() == "test-session")
398 .unwrap();
399
400 // Step 3: POST to do_something
401 // - adds new session state: {"counter": 1, "user_id": "ferris" }
402 // - set-cookie actix-session should be in response (session cookie #2)
403 // - response should be: {"counter": 1, "user_id": None}
404 let req_3 = srv.post("/do_something").cookie(cookie_1.clone()).send();
405 let mut resp_3 = req_3.await.unwrap();
406 let result_3 = resp_3.json::<IndexResponse>().await.unwrap();
407 assert_eq!(
408 result_3,
409 IndexResponse {
410 user_id: Some("ferris".into()),
411 counter: 1
412 }
413 );
414 let cookie_2 = resp_3
415 .cookies()
416 .unwrap()
417 .clone()
418 .into_iter()
419 .find(|c| c.name() == "test-session")
420 .unwrap();
421
422 // Step 4: GET using a existing user id
423 // - response should be: {"counter": 3, "user_id": "ferris"}
424 let req_4 = srv.get("/protected/count").cookie(cookie_2.clone()).send();
425 let mut resp_4 = req_4.await.unwrap();
426 let result_4 = resp_4.json::<IndexResponse>().await.unwrap();
427 assert_eq!(
428 result_4,
429 IndexResponse {
430 user_id: Some("ferris".into()),
431 counter: 1
432 }
433 );
434 }
435
436 pub(super) async fn complex_workflow<F, Store>(
437 store_builder: F,
438 is_invalidation_supported: bool,
439 policy: CookieContentSecurity,
440 ) where
441 Store: SessionStore + 'static,
442 F: Fn() -> Store + Clone + Send + 'static,
443 {
444 let session_ttl = time::Duration::days(7);
445 let srv = actix_test::start(move || {
446 App::new()
447 .wrap(
448 SessionMiddleware::builder(store_builder(), key())
449 .cookie_name("test-session".into())
450 .cookie_content_security(policy)
451 .session_lifecycle(
452 PersistentSession::default().session_ttl(session_ttl),
453 )
454 .build(),
455 )
456 .wrap(middleware::Logger::default())
457 .service(resource("/").route(get().to(index)))
458 .service(resource("/do_something").route(post().to(do_something)))
459 .service(resource("/login").route(post().to(login)))
460 .service(resource("/logout").route(post().to(logout)))
461 });
462
463 // Step 1: GET index
464 // - set-cookie actix-session should NOT be in response (session data is empty)
465 // - response should be: {"counter": 0, "user_id": None}
466 let req_1a = srv.get("/").send();
467 let mut resp_1 = req_1a.await.unwrap();
468 assert!(resp_1.cookies().unwrap().is_empty());
469 let result_1 = resp_1.json::<IndexResponse>().await.unwrap();
470 assert_eq!(
471 result_1,
472 IndexResponse {
473 user_id: None,
474 counter: 0
475 }
476 );
477
478 // Step 2: POST to do_something
479 // - adds new session state in redis: {"counter": 1}
480 // - set-cookie actix-session should be in response (session cookie #1)
481 // - response should be: {"counter": 1, "user_id": None}
482 let req_2 = srv.post("/do_something").send();
483 let mut resp_2 = req_2.await.unwrap();
484 let result_2 = resp_2.json::<IndexResponse>().await.unwrap();
485 assert_eq!(
486 result_2,
487 IndexResponse {
488 user_id: None,
489 counter: 1
490 }
491 );
492 let cookie_1 = resp_2
493 .cookies()
494 .unwrap()
495 .clone()
496 .into_iter()
497 .find(|c| c.name() == "test-session")
498 .unwrap();
499 assert_eq!(cookie_1.max_age(), Some(session_ttl));
500
501 // Step 3: GET index, including session cookie #1 in request
502 // - set-cookie will *not* be in response
503 // - response should be: {"counter": 1, "user_id": None}
504 let req_3 = srv.get("/").cookie(cookie_1.clone()).send();
505 let mut resp_3 = req_3.await.unwrap();
506 assert!(resp_3.cookies().unwrap().is_empty());
507 let result_3 = resp_3.json::<IndexResponse>().await.unwrap();
508 assert_eq!(
509 result_3,
510 IndexResponse {
511 user_id: None,
512 counter: 1
513 }
514 );
515
516 // Step 4: POST again to do_something, including session cookie #1 in request
517 // - set-cookie will be in response (session cookie #2)
518 // - updates session state: {"counter": 2}
519 // - response should be: {"counter": 2, "user_id": None}
520 let req_4 = srv.post("/do_something").cookie(cookie_1.clone()).send();
521 let mut resp_4 = req_4.await.unwrap();
522 let result_4 = resp_4.json::<IndexResponse>().await.unwrap();
523 assert_eq!(
524 result_4,
525 IndexResponse {
526 user_id: None,
527 counter: 2
528 }
529 );
530 let cookie_2 = resp_4
531 .cookies()
532 .unwrap()
533 .clone()
534 .into_iter()
535 .find(|c| c.name() == "test-session")
536 .unwrap();
537 assert_eq!(cookie_2.max_age(), cookie_1.max_age());
538
539 // Step 5: POST to login, including session cookie #2 in request
540 // - set-cookie actix-session will be in response (session cookie #3)
541 // - updates session state: {"counter": 2, "user_id": "ferris"}
542 let req_5 = srv
543 .post("/login")
544 .cookie(cookie_2.clone())
545 .send_json(&json!({"user_id": "ferris"}));
546 let mut resp_5 = req_5.await.unwrap();
547 let cookie_3 = resp_5
548 .cookies()
549 .unwrap()
550 .clone()
551 .into_iter()
552 .find(|c| c.name() == "test-session")
553 .unwrap();
554 assert_ne!(cookie_2.value(), cookie_3.value());
555
556 let result_5 = resp_5.json::<IndexResponse>().await.unwrap();
557 assert_eq!(
558 result_5,
559 IndexResponse {
560 user_id: Some("ferris".into()),
561 counter: 2
562 }
563 );
564
565 // Step 6: GET index, including session cookie #3 in request
566 // - response should be: {"counter": 2, "user_id": "ferris"}
567 let req_6 = srv.get("/").cookie(cookie_3.clone()).send();
568 let mut resp_6 = req_6.await.unwrap();
569 let result_6 = resp_6.json::<IndexResponse>().await.unwrap();
570 assert_eq!(
571 result_6,
572 IndexResponse {
573 user_id: Some("ferris".into()),
574 counter: 2
575 }
576 );
577
578 // Step 7: POST again to do_something, including session cookie #3 in request
579 // - updates session state: {"counter": 3, "user_id": "ferris"}
580 // - response should be: {"counter": 3, "user_id": "ferris"}
581 let req_7 = srv.post("/do_something").cookie(cookie_3.clone()).send();
582 let mut resp_7 = req_7.await.unwrap();
583 let result_7 = resp_7.json::<IndexResponse>().await.unwrap();
584 assert_eq!(
585 result_7,
586 IndexResponse {
587 user_id: Some("ferris".into()),
588 counter: 3
589 }
590 );
591
592 // Step 8: GET index, including session cookie #2 in request
593 // If invalidation is supported, no state will be found associated to this session.
594 // If invalidation is not supported, the old state will still be retrieved.
595 let req_8 = srv.get("/").cookie(cookie_2.clone()).send();
596 let mut resp_8 = req_8.await.unwrap();
597 if is_invalidation_supported {
598 assert!(resp_8.cookies().unwrap().is_empty());
599 let result_8 = resp_8.json::<IndexResponse>().await.unwrap();
600 assert_eq!(
601 result_8,
602 IndexResponse {
603 user_id: None,
604 counter: 0
605 }
606 );
607 } else {
608 let result_8 = resp_8.json::<IndexResponse>().await.unwrap();
609 assert_eq!(
610 result_8,
611 IndexResponse {
612 user_id: None,
613 counter: 2
614 }
615 );
616 }
617
618 // Step 9: POST to logout, including session cookie #3
619 // - set-cookie actix-session will be in response with session cookie #3
620 // invalidation logic
621 let req_9 = srv.post("/logout").cookie(cookie_3.clone()).send();
622 let resp_9 = req_9.await.unwrap();
623 let cookie_3 = resp_9
624 .cookies()
625 .unwrap()
626 .clone()
627 .into_iter()
628 .find(|c| c.name() == "test-session")
629 .unwrap();
630 assert_eq!(0, cookie_3.max_age().map(|t| t.whole_seconds()).unwrap());
631 assert_eq!("/", cookie_3.path().unwrap());
632
633 // Step 10: GET index, including session cookie #3 in request
634 // - set-cookie actix-session should NOT be in response if invalidation is supported
635 // - response should be: {"counter": 0, "user_id": None}
636 let req_10 = srv.get("/").cookie(cookie_3.clone()).send();
637 let mut resp_10 = req_10.await.unwrap();
638 if is_invalidation_supported {
639 assert!(resp_10.cookies().unwrap().is_empty());
640 }
641 let result_10 = resp_10.json::<IndexResponse>().await.unwrap();
642 assert_eq!(
643 result_10,
644 IndexResponse {
645 user_id: None,
646 counter: 0
647 }
648 );
649 }
650
651 #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
652 pub struct IndexResponse {
653 user_id: Option<String>,
654 counter: i32,
655 }
656
657 async fn index(session: Session) -> Result<HttpResponse> {
658 let user_id: Option<String> = session.get::<String>("user_id").unwrap();
659 let counter: i32 = session
660 .get::<i32>("counter")
661 .unwrap_or(Some(0))
662 .unwrap_or(0);
663
664 Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter }))
665 }
666
667 async fn do_something(session: Session) -> Result<HttpResponse> {
668 let user_id: Option<String> = session.get::<String>("user_id").unwrap();
669 let counter: i32 = session
670 .get::<i32>("counter")
671 .unwrap_or(Some(0))
672 .map_or(1, |inner| inner + 1);
673 session.insert("counter", counter)?;
674
675 Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter }))
676 }
677
678 async fn count(session: Session) -> Result<HttpResponse> {
679 let user_id: Option<String> = session.get::<String>("user_id").unwrap();
680 let counter: i32 = session.get::<i32>("counter").unwrap().unwrap();
681
682 Ok(HttpResponse::Ok().json(&IndexResponse { user_id, counter }))
683 }
684
685 #[derive(Deserialize)]
686 struct Identity {
687 user_id: String,
688 }
689
690 async fn login(user_id: web::Json<Identity>, session: Session) -> Result<HttpResponse> {
691 let id = user_id.into_inner().user_id;
692 session.insert("user_id", &id)?;
693 session.renew();
694
695 let counter: i32 = session
696 .get::<i32>("counter")
697 .unwrap_or(Some(0))
698 .unwrap_or(0);
699
700 Ok(HttpResponse::Ok().json(&IndexResponse {
701 user_id: Some(id),
702 counter,
703 }))
704 }
705
706 async fn logout(session: Session) -> Result<HttpResponse> {
707 let id: Option<String> = session.get("user_id")?;
708
709 let body = if let Some(id) = id {
710 session.purge();
711 format!("Logged out: {id}")
712 } else {
713 "Could not log out anonymous user".to_owned()
714 };
715
716 Ok(HttpResponse::Ok().body(body))
717 }
718
719 trait ServiceResponseExt {
720 fn get_cookie(&self, cookie_name: &str) -> Option<actix_web::cookie::Cookie<'_>>;
721 }
722
723 impl ServiceResponseExt for ServiceResponse {
724 fn get_cookie(&self, cookie_name: &str) -> Option<actix_web::cookie::Cookie<'_>> {
725 self.response().cookies().find(|c| c.name() == cookie_name)
726 }
727 }
728 }
729}