Bug: feat: Add Cookie in response header



Using cookies for storing login token instead of local storage.
Add `set-cookie` response header with json-web-token as its value.  
diff --git a/Cargo.lock b/Cargo.lock
index 2e4d3324d2e..7c7b2b9a761 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -224,7 +224,7 @@ dependencies = [
  "bytes 1.5.0",
  "bytestring",
  "cfg-if 1.0.0",
- "cookie",
+ "cookie 0.16.2",
  "derive_more",
  "encoding_rs",
  "futures-core",
@@ -616,7 +616,7 @@ dependencies = [
  "base64 0.21.5",
  "bytes 1.5.0",
  "cfg-if 1.0.0",
- "cookie",
+ "cookie 0.16.2",
  "derive_more",
  "futures-core",
  "futures-util",
@@ -1763,6 +1763,16 @@ dependencies = [
  "version_check",
 ]
 
+[[package]]
+name = "cookie"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8"
+dependencies = [
+ "time",
+ "version_check",
+]
+
 [[package]]
 name = "cookie-factory"
 version = "0.3.2"
@@ -2524,7 +2534,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "65f0fbe245d714b596ba5802b46f937f5ce68dcae0f32f9a70b5c3b04d3c6f64"
 dependencies = [
  "base64 0.13.1",
- "cookie",
+ "cookie 0.16.2",
  "futures-core",
  "futures-util",
  "http",
@@ -5219,6 +5229,7 @@ dependencies = [
  "common_enums",
  "common_utils",
  "config",
+ "cookie 0.18.0",
  "currency_conversion",
  "data_models",
  "derive_deref",
@@ -6414,7 +6425,7 @@ dependencies = [
  "async-trait",
  "base64 0.13.1",
  "chrono",
- "cookie",
+ "cookie 0.16.2",
  "fantoccini",
  "futures 0.3.28",
  "http",
@@ -7461,7 +7472,7 @@ checksum = "9973cb72c8587d5ad5efdb91e663d36177dc37725e6c90ca86c626b0cc45c93f"
 dependencies = [
  "base64 0.13.1",
  "bytes 1.5.0",
- "cookie",
+ "cookie 0.16.2",
  "http",
  "log",
  "serde",
diff --git a/crates/masking/src/maskable.rs b/crates/masking/src/maskable.rs
index 3469e1dd746..7969c9ab8e4 100644
--- a/crates/masking/src/maskable.rs
+++ b/crates/masking/src/maskable.rs
@@ -58,6 +58,24 @@ impl<T: Eq + PartialEq + Clone> Maskable<T> {
     pub fn new_normal(item: T) -> Self {
         Self::Normal(item)
     }
+
+    ///
+    /// Checks whether the data is masked.
+    /// Returns `true` if the data is wrapped in the `Masked` variant,
+    /// returns `false` otherwise.
+    ///
+    pub fn is_masked(&self) -> bool {
+        matches!(self, Self::Masked(_))
+    }
+
+    ///
+    /// Checks whether the data is normal (not masked).
+    /// Returns `true` if the data is wrapped in the `Normal` variant,
+    /// returns `false` otherwise.
+    ///
+    pub fn is_normal(&self) -> bool {
+        matches!(self, Self::Normal(_))
+    }
 }
 
 /// Trait for providing a method on custom types for constructing `Maskable`
diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml
index 9cb7913211f..f001609ee88 100644
--- a/crates/router/Cargo.toml
+++ b/crates/router/Cargo.toml
@@ -46,6 +46,7 @@ blake3 = "1.3.3"
 bytes = "1.4.0"
 clap = { version = "4.3.2", default-features = false, features = ["std", "derive", "help", "usage"] }
 config = { version = "0.13.3", features = ["toml"] }
+cookie = "0.18.0"
 diesel = { version = "2.1.0", features = ["postgres"] }
 digest = "0.9"
 dyn-clone = "1.0.11"
diff --git a/crates/router/src/consts.rs b/crates/router/src/consts.rs
index 26598493c87..70add106762 100644
--- a/crates/router/src/consts.rs
+++ b/crates/router/src/consts.rs
@@ -68,6 +68,8 @@ pub const LOCKER_REDIS_EXPIRY_SECONDS: u32 = 60 * 15; // 15 minutes
 
 pub const JWT_TOKEN_TIME_IN_SECS: u64 = 60 * 60 * 24 * 2; // 2 days
 
+pub const JWT_TOKEN_COOKIE_NAME: &str = "login_token";
+
 pub const USER_BLACKLIST_PREFIX: &str = "BU_";
 
 pub const ROLE_BLACKLIST_PREFIX: &str = "BR_";
diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs
index 3cb11d1c0e9..9dd7df66fa6 100644
--- a/crates/router/src/core/payments/transformers.rs
+++ b/crates/router/src/core/payments/transformers.rs
@@ -5,6 +5,7 @@ use common_enums::RequestIncrementalAuthorization;
 use common_utils::{consts::X_HS_LATENCY, fp_utils};
 use diesel_models::ephemeral_key;
 use error_stack::{report, IntoReport, ResultExt};
+use masking::Maskable;
 use router_env::{instrument, tracing};
 
 use super::{flows::Feature, PaymentData};
@@ -466,20 +467,25 @@ where
         .map(|status_code| {
             vec![(
                 "connector_http_status_code".to_string(),
-                status_code.to_string(),
+                Maskable::new_normal(status_code.to_string()),
             )]
         })
         .unwrap_or_default();
     if let Some(payment_confirm_source) = payment_intent.payment_confirm_source {
         headers.push((
             "payment_confirm_source".to_string(),
-            payment_confirm_source.to_string(),
+            Maskable::new_normal(payment_confirm_source.to_string()),
         ))
     }
 
     headers.extend(
         external_latency
-            .map(|latency| vec![(X_HS_LATENCY.to_string(), latency.to_string())])
+            .map(|latency| {
+                vec![(
+                    X_HS_LATENCY.to_string(),
+                    Maskable::new_normal(latency.to_string()),
+                )]
+            })
             .unwrap_or_default(),
     );
 
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index c1e10ddfabf..703c6443160 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -93,12 +93,13 @@ pub async fn signup(
             UserStatus::Active,
         )
         .await?;
-    let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
     utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
 
-    Ok(ApplicationResponse::Json(
-        utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?,
-    ))
+    let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
+    let response =
+        utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?;
+
+    auth::cookies::set_cookie_response(response, token)
 }
 
 pub async fn signin_without_invite_checks(
@@ -121,12 +122,12 @@ pub async fn signin_without_invite_checks(
     user_from_db.compare_password(request.password)?;
 
     let user_role = user_from_db.get_role_from_db(state.clone()).await?;
-    let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
     utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
 
-    Ok(ApplicationResponse::Json(
-        utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?,
-    ))
+    let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
+    let response =
+        utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?;
+    auth::cookies::set_cookie_response(response, token)
 }
 
 pub async fn signin(
@@ -168,9 +169,9 @@ pub async fn signin(
             .await?
         };
 
-    Ok(ApplicationResponse::Json(
-        signin_strategy.get_signin_response(&state).await?,
-    ))
+    let response = signin_strategy.get_signin_response(&state).await?;
+    let token = utils::user::get_token_from_signin_response(&response);
+    auth::cookies::set_cookie_response(response, token)
 }
 
 #[cfg(feature = "email")]
@@ -271,7 +272,7 @@ pub async fn connect_account(
 
 pub async fn signout(state: AppState, user_from_token: auth::UserFromToken) -> UserResponse<()> {
     auth::blacklist::insert_user_in_blacklist(&state, &user_from_token.user_id).await?;
-    Ok(ApplicationResponse::StatusOk)
+    auth::cookies::remove_cookie_response()
 }
 
 pub async fn change_password(
@@ -971,14 +972,14 @@ pub async fn accept_invite_from_email(
     utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &update_status_result)
         .await;
 
-    Ok(ApplicationResponse::Json(
-        utils::user::get_dashboard_entry_response(
-            &state,
-            user_from_db,
-            update_status_result,
-            token,
-        )?,
-    ))
+    let response = utils::user::get_dashboard_entry_response(
+        &state,
+        user_from_db,
+        update_status_result,
+        token.clone(),
+    )?;
+
+    auth::cookies::set_cookie_response(response, token)
 }
 
 pub async fn create_internal_user(
@@ -1130,17 +1131,17 @@ pub async fn switch_merchant_id(
         (token, user_role.role_id.clone())
     };
 
-    Ok(ApplicationResponse::Json(
-        user_api::DashboardEntryResponse {
-            token,
-            name: user.get_name(),
-            email: user.get_email(),
-            user_id: user.get_user_id().to_string(),
-            verification_days_left: None,
-            user_role: role_id,
-            merchant_id: request.merchant_id,
-        },
-    ))
+    let response = user_api::DashboardEntryResponse {
+        token: token.clone(),
+        name: user.get_name(),
+        email: user.get_email(),
+        user_id: user.get_user_id().to_string(),
+        verification_days_left: None,
+        user_role: role_id,
+        merchant_id: request.merchant_id,
+    };
+
+    auth::cookies::set_cookie_response(response, token)
 }
 
 pub async fn create_merchant_account(
@@ -1318,9 +1319,10 @@ pub async fn verify_email_without_invite_checks(
     let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
     utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
 
-    Ok(ApplicationResponse::Json(
-        utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?,
-    ))
+    let response =
+        utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?;
+
+    auth::cookies::set_cookie_response(response, token)
 }
 
 #[cfg(feature = "email")]
@@ -1373,9 +1375,9 @@ pub async fn verify_email(
         .await
         .map_err(|e| logger::error!(?e));
 
-    Ok(ApplicationResponse::Json(
-        signin_strategy.get_signin_response(&state).await?,
-    ))
+    let response = signin_strategy.get_signin_response(&state).await?;
+    let token = utils::user::get_token_from_signin_response(&response);
+    auth::cookies::set_cookie_response(response, token)
 }
 
 #[cfg(feature = "email")]
diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs
index b81b980c07a..587e0e04435 100644
--- a/crates/router/src/core/user_role.rs
+++ b/crates/router/src/core/user_role.rs
@@ -158,12 +158,13 @@ pub async fn transfer_org_ownership(
         .await
         .to_not_found_response(UserErrors::InvalidRoleOperation)?;
 
-    let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
     utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
 
-    Ok(ApplicationResponse::Json(
-        utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?,
-    ))
+    let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
+    let response =
+        utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?;
+
+    auth::cookies::set_cookie_response(response, token)
 }
 
 pub async fn accept_invitation(
@@ -202,12 +203,17 @@ pub async fn accept_invitation(
             .change_context(UserErrors::InternalServerError)?
             .into();
 
-        let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
         utils::user_role::set_role_permissions_in_cache_by_user_role(&state, &user_role).await;
 
-        return Ok(ApplicationResponse::Json(
-            utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token)?,
-        ));
+        let token = utils::user::generate_jwt_auth_token(&state, &user_from_db, &user_role).await?;
+
+        let response = utils::user::get_dashboard_entry_response(
+            &state,
+            user_from_db,
+            user_role,
+            token.clone(),
+        )?;
+        return auth::cookies::set_cookie_response(response, token);
     }
 
     Ok(ApplicationResponse::StatusOk)
diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs
index bc8e3b99d23..00558e6c4f4 100644
--- a/crates/router/src/services/api.rs
+++ b/crates/router/src/services/api.rs
@@ -9,7 +9,10 @@ use std::{
     time::{Duration, Instant},
 };
 
-use actix_web::{body, web, FromRequest, HttpRequest, HttpResponse, Responder, ResponseError};
+use actix_web::{
+    body, http::header::HeaderValue, web, FromRequest, HttpRequest, HttpResponse, Responder,
+    ResponseError,
+};
 use api_models::enums::{CaptureMethod, PaymentMethodType};
 pub use client::{proxy_bypass_urls, ApiClient, MockApiClient, ProxyClient};
 use common_enums::Currency;
@@ -20,7 +23,7 @@ use common_utils::{
     request::RequestContent,
 };
 use error_stack::{report, IntoReport, Report, ResultExt};
-use masking::{PeekInterface, Secret};
+use masking::{Maskable, PeekInterface, Secret};
 use router_env::{instrument, tracing, tracing_actix_web::RequestId, Tag};
 use serde::Serialize;
 use serde_json::json;
@@ -110,7 +113,7 @@ pub trait ConnectorIntegration<T, Req, Resp>: ConnectorIntegrationAny<T, Req, Re
         &self,
         _req: &types::RouterData<T, Req, Resp>,
         _connectors: &Connectors,
-    ) -> CustomResult<Vec<(String, request::Maskable<String>)>, errors::ConnectorError> {
+    ) -> CustomResult<Vec<(String, Maskable<String>)>, errors::ConnectorError> {
         Ok(vec![])
     }
 
@@ -847,7 +850,7 @@ pub enum ApplicationResponse<R> {
     Form(Box<RedirectionFormData>),
     PaymentLinkForm(Box<PaymentLinkAction>),
     FileData((Vec<u8>, mime::Mime)),
-    JsonWithHeaders((R, Vec<(String, String)>)),
+    JsonWithHeaders((R, Vec<(String, Maskable<String>)>)),
 }
 
 #[derive(Debug, Eq, PartialEq)]
@@ -1046,7 +1049,7 @@ where
                 );
 
                 if let Some((_, value)) = headers.iter().find(|(key, _)| key == X_HS_LATENCY) {
-                    if let Ok(external_latency) = value.parse::<u128>() {
+                    if let Ok(external_latency) = value.clone().into_inner().parse::<u128>() {
                         overhead_latency.replace(external_latency);
                     }
                 }
@@ -1126,7 +1129,7 @@ where
 
     let incoming_request_header = request.headers();
 
-    let incoming_header_to_log: HashMap<String, http::header::HeaderValue> =
+    let incoming_header_to_log: HashMap<String, HeaderValue> =
         incoming_request_header
             .iter()
             .fold(HashMap::new(), |mut acc, (key, value)| {
@@ -1333,21 +1336,33 @@ pub fn http_server_error_json_response<T: body::MessageBody + 'static>(
 
 pub fn http_response_json_with_headers<T: body::MessageBody + 'static>(
     response: T,
-    mut headers: Vec<(String, String)>,
+    headers: Vec<(String, Maskable<String>)>,
     request_duration: Option<Duration>,
 ) -> HttpResponse {
     let mut response_builder = HttpResponse::Ok();
-
-    for (name, value) in headers.iter_mut() {
-        if name == X_HS_LATENCY {
+    for (header_name, header_value) in headers {
+        let is_sensitive_header = header_value.is_masked();
+        let mut header_value = header_value.into_inner();
+        if header_name == X_HS_LATENCY {
             if let Some(request_duration) = request_duration {
-                if let Ok(external_latency) = value.parse::<u128>() {
+                if let Ok(external_latency) = header_value.parse::<u128>() {
                     let updated_duration = request_duration.as_millis() - external_latency;
-                    *value = updated_duration.to_string();
+                    header_value = updated_duration.to_string();
                 }
             }
         }
-        response_builder.append_header((name.clone(), value.clone()));
+        let mut header_value = match HeaderValue::from_str(header_value.as_str()) {
+            Ok(header_value) => header_value,
+            Err(e) => {
+                logger::error!(?e);
+                return http_server_error_json_response("Something Went Wrong");
+            }
+        };
+
+        if is_sensitive_header {
+            header_value.set_sensitive(true);
+        }
+        response_builder.append_header((header_name, header_value));
     }
 
     response_builder
diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs
index 7cf1855b441..eb115f5701c 100644
--- a/crates/router/src/services/authentication.rs
+++ b/crates/router/src/services/authentication.rs
@@ -33,6 +33,8 @@ use crate::{
     utils::OptionExt,
 };
 pub mod blacklist;
+#[cfg(feature = "olap")]
+pub mod cookies;
 
 #[derive(Clone, Debug)]
 pub struct AuthenticationData {
diff --git a/crates/router/src/services/authentication/cookies.rs b/crates/router/src/services/authentication/cookies.rs
new file mode 100644
index 00000000000..d7fc4c10315
--- /dev/null
+++ b/crates/router/src/services/authentication/cookies.rs
@@ -0,0 +1,62 @@
+use cookie::{
+    time::{Duration, OffsetDateTime},
+    Cookie, SameSite,
+};
+use masking::{ExposeInterface, Mask, Secret};
+
+use crate::{
+    consts::{JWT_TOKEN_COOKIE_NAME, JWT_TOKEN_TIME_IN_SECS},
+    core::errors::{UserErrors, UserResponse},
+    services::ApplicationResponse,
+};
+
+pub fn set_cookie_response<R>(response: R, token: Secret<String>) -> UserResponse<R> {
+    let jwt_expiry_in_seconds = JWT_TOKEN_TIME_IN_SECS
+        .try_into()
+        .map_err(|_| UserErrors::InternalServerError)?;
+    let (expiry, max_age) = get_expiry_and_max_age_from_seconds(jwt_expiry_in_seconds);
+
+    let header_value = create_cookie(token, expiry, max_age)
+        .to_string()
+        .into_masked();
+    let header_key = get_cookie_header();
+    let header = vec![(header_key, header_value)];
+
+    Ok(ApplicationResponse::JsonWithHeaders((response, header)))
+}
+
+pub fn remove_cookie_response() -> UserResponse<()> {
+    let (expiry, max_age) = get_expiry_and_max_age_from_seconds(0);
+
+    let header_key = get_cookie_header();
+    let header_value = create_cookie("".to_string().into(), expiry, max_age)
+        .to_string()
+        .into_masked();
+    let header = vec![(header_key, header_value)];
+    Ok(ApplicationResponse::JsonWithHeaders(((), header)))
+}
+
+fn create_cookie<'c>(
+    token: Secret<String>,
+    expires: OffsetDateTime,
+    max_age: Duration,
+) -> Cookie<'c> {
+    Cookie::build((JWT_TOKEN_COOKIE_NAME, token.expose()))
+        .http_only(true)
+        .secure(true)
+        .same_site(SameSite::Strict)
+        .path("/")
+        .expires(expires)
+        .max_age(max_age)
+        .build()
+}
+
+fn get_expiry_and_max_age_from_seconds(seconds: i64) -> (OffsetDateTime, Duration) {
+    let max_age = Duration::seconds(seconds);
+    let expiry = OffsetDateTime::now_utc().saturating_add(max_age);
+    (expiry, max_age)
+}
+
+fn get_cookie_header() -> String {
+    actix_http::header::SET_COOKIE.to_string()
+}
diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs
index cfab331f148..e97d5ae1efd 100644
--- a/crates/router/src/utils/user.rs
+++ b/crates/router/src/utils/user.rs
@@ -170,3 +170,10 @@ pub async fn get_user_from_db_by_email(
         .await
         .map(UserFromStorage::from)
 }
+
+pub fn get_token_from_signin_response(resp: &user_api::SignInResponse) -> Secret<String> {
+    match resp {
+        user_api::SignInResponse::DashboardEntry(data) => data.token.clone(),
+        user_api::SignInResponse::MerchantSelect(data) => data.token.clone(),
+    }
+}

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [X] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
This is the first PR for cookie implementation.
In this pr, `set-cookie` header has been added to response header, where value of the header is jwt.
<!-- Describe your changes in detail -->


### Additional Changes

- [X] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
Storing JWT in cookies instead of local storage
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
Following apis should have `set-cookie` header with jwt as value.

```
curl --location '<URL>/user/signup' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "email",
    "password": "password"
}'
```
```
curl --location '<URL>/user/v2/signin' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "email",
    "password": "password"
}'
```
```
curl --location '<URL>/user/signin' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "email",
    "password": "password"
}'
```
```
curl --location '<URL>/user/verify_email' \
--header 'Content-Type: application/json' \
--data '{
    "token": "email token"
}'
```
```
curl --location '<URL>/user/v2/verify_email' \
--header 'Content-Type: application/json' \
--data '{
    "token": "email token"
}'
```
```
curl --location '<URL>/user/accept_invite_from_email' \
--header 'Content-Type: application/json' \
--data '{
    "token": "email token"
}'
```
```
curl --location '<URL>/user/switch_merchant' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <JWT>' \
--data '{
    "merchant_id": "merchant_1707460394"
}'
```
```
curl --location '<URL>/user/user/transfer_ownership' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <JWT>' \
--data-raw '{
    "email": "email"
}'
```
```
curl --location '<URL>/user/user/invite/accept' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <JWT>' \
--data '{
    "merchant_ids": [
        "merchant_1705922092",
        "merchant_1705414514" 
    ],
    "need_dashboard_entry_response": true
}'
```
Following api should have `set-cookie` header with `""` (empty value) as value.
```
curl --location '<URL>/user/signout' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer <JWT>' \
--data-raw '{
    "email": "email"
}'
```

<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [X] I formatted the code `cargo +nightly fmt --all`
- [X] I addressed lints thrown by `cargo clippy`
- [X] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable
