Bug: feat: decision manger for user flows



As we are going to split all the APIs, we need a way to get the next API that dashboard should go to using the current flow/origin.

We need a decision manager that can take the current flow and origin and give the next flow.
diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs
index 8f017ca8c20..594b60b5816 100644
--- a/crates/api_models/src/events/user.rs
+++ b/crates/api_models/src/events/user.rs
@@ -14,9 +14,9 @@ use crate::user::{
     CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest,
     GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse,
     InviteUserRequest, ListUsersResponse, ReInviteUserRequest, ResetPasswordRequest,
-    SendVerifyEmailRequest, SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest,
-    SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, UserMerchantCreate,
-    VerifyEmailRequest,
+    SendVerifyEmailRequest, SignInResponse, SignInWithTokenResponse, SignUpRequest,
+    SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest,
+    UserMerchantCreate, VerifyEmailRequest,
 };
 
 impl ApiEventMetric for DashboardEntryResponse {
@@ -62,6 +62,7 @@ common_utils::impl_misc_api_event_type!(
     SignInResponse,
     UpdateUserAccountDetailsRequest,
     GetUserDetailsResponse,
+    SignInWithTokenResponse,
     GetUserRoleDetailsRequest,
     GetUserRoleDetailsResponse
 );
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index 700c27461cd..b4d53a92c1a 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -1,4 +1,4 @@
-use common_enums::{PermissionGroup, RoleScope};
+use common_enums::{PermissionGroup, RoleScope, TokenPurpose};
 use common_utils::{crypto::OptionalEncryptableName, pii};
 use masking::Secret;
 
@@ -213,3 +213,21 @@ pub struct UpdateUserAccountDetailsRequest {
     pub name: Option<Secret<String>>,
     pub preferred_merchant_id: Option<String>,
 }
+
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct TokenOnlyQueryParam {
+    pub token_only: Option<bool>,
+}
+
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct TokenResponse {
+    pub token: Secret<String>,
+    pub token_type: TokenPurpose,
+}
+
+#[derive(Debug, serde::Serialize)]
+#[serde(untagged)]
+pub enum SignInWithTokenResponse {
+    Token(TokenResponse),
+    SignInResponse(SignInResponse),
+}
diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs
index 18545eb4a4d..3c86064731b 100644
--- a/crates/common_enums/src/enums.rs
+++ b/crates/common_enums/src/enums.rs
@@ -2706,3 +2706,17 @@ pub enum BankHolderType {
     Personal,
     Business,
 }
+
+#[derive(Debug, Clone, PartialEq, Eq, strum::Display, serde::Deserialize, serde::Serialize)]
+#[strum(serialize_all = "snake_case")]
+#[serde(rename_all = "snake_case")]
+pub enum TokenPurpose {
+    #[serde(rename = "totp")]
+    #[strum(serialize = "totp")]
+    TOTP,
+    VerifyEmail,
+    AcceptInvitationFromEmail,
+    ResetPassword,
+    AcceptInvite,
+    UserInfo,
+}
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 2e439a6eb52..0ae1b162e0e 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -123,7 +123,7 @@ pub async fn signup(
 pub async fn signin(
     state: AppState,
     request: user_api::SignInRequest,
-) -> UserResponse<user_api::SignInResponse> {
+) -> UserResponse<user_api::SignInWithTokenResponse> {
     let user_from_db: domain::UserFromStorage = state
         .store
         .find_user_by_email(&request.email)
@@ -161,6 +161,48 @@ pub async fn signin(
 
     let response = signin_strategy.get_signin_response(&state).await?;
     let token = utils::user::get_token_from_signin_response(&response);
+    auth::cookies::set_cookie_response(
+        user_api::SignInWithTokenResponse::SignInResponse(response),
+        token,
+    )
+}
+
+pub async fn signin_token_only_flow(
+    state: AppState,
+    request: user_api::SignInRequest,
+) -> UserResponse<user_api::SignInWithTokenResponse> {
+    let user_from_db: domain::UserFromStorage = state
+        .store
+        .find_user_by_email(&request.email)
+        .await
+        .to_not_found_response(UserErrors::InvalidCredentials)?
+        .into();
+
+    user_from_db.compare_password(request.password)?;
+
+    let next_flow =
+        domain::NextFlow::from_origin(domain::Origin::SignIn, user_from_db.clone(), &state).await?;
+
+    let token = match next_flow.get_flow() {
+        domain::UserFlow::SPTFlow(spt_flow) => spt_flow.generate_spt(&state, &next_flow).await,
+        domain::UserFlow::JWTFlow(jwt_flow) => {
+            #[cfg(feature = "email")]
+            {
+                user_from_db.get_verification_days_left(&state)?;
+            }
+
+            let user_role = user_from_db
+                .get_preferred_or_active_user_role_from_db(&state)
+                .await
+                .to_not_found_response(UserErrors::InternalServerError)?;
+            jwt_flow.generate_jwt(&state, &next_flow, &user_role).await
+        }
+    }?;
+
+    let response = user_api::SignInWithTokenResponse::Token(user_api::TokenResponse {
+        token: token.clone(),
+        token_type: next_flow.get_flow().into(),
+    });
     auth::cookies::set_cookie_response(response, token)
 }
 
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index 0657d781b08..4d841913bc0 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -76,15 +76,23 @@ pub async fn user_signin(
     state: web::Data<AppState>,
     http_req: HttpRequest,
     json_payload: web::Json<user_api::SignInRequest>,
+    query: web::Query<user_api::TokenOnlyQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::UserSignIn;
     let req_payload = json_payload.into_inner();
+    let is_token_only = query.into_inner().token_only;
     Box::pin(api::server_wrap(
         flow.clone(),
         state,
         &http_req,
         req_payload.clone(),
-        |state, _, req_body, _| user_core::signin(state, req_body),
+        |state, _, req_body, _| async move {
+            if let Some(true) = is_token_only {
+                user_core::signin_token_only_flow(state, req_body).await
+            } else {
+                user_core::signin(state, req_body).await
+            }
+        },
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs
index 7247c858f4d..59c07b79855 100644
--- a/crates/router/src/routes/user_role.rs
+++ b/crates/router/src/routes/user_role.rs
@@ -1,5 +1,6 @@
 use actix_web::{web, HttpRequest, HttpResponse};
 use api_models::user_role::{self as user_role_api, role as role_api};
+use common_enums::TokenPurpose;
 use router_env::Flow;
 
 use super::AppState;
@@ -214,7 +215,7 @@ pub async fn accept_invitation(
         &req,
         payload,
         user_role_core::accept_invitation,
-        &auth::SinglePurposeJWTAuth(auth::Purpose::AcceptInvite),
+        &auth::SinglePurposeJWTAuth(TokenPurpose::AcceptInvite),
         api_locking::LockAction::NotApplicable,
     ))
     .await
diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs
index c49ad81d919..5f15474115d 100644
--- a/crates/router/src/services/authentication.rs
+++ b/crates/router/src/services/authentication.rs
@@ -4,6 +4,7 @@ use api_models::{
     payments,
 };
 use async_trait::async_trait;
+use common_enums::TokenPurpose;
 use common_utils::date_time;
 use error_stack::{report, ResultExt};
 use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
@@ -66,7 +67,7 @@ pub enum AuthenticationType {
     },
     SinglePurposeJWT {
         user_id: String,
-        purpose: Purpose,
+        purpose: TokenPurpose,
     },
     MerchantId {
         merchant_id: String,
@@ -113,28 +114,28 @@ impl AuthenticationType {
     }
 }
 
+#[cfg(feature = "olap")]
 #[derive(Clone, Debug)]
 pub struct UserFromSinglePurposeToken {
     pub user_id: String,
+    pub origin: domain::Origin,
 }
 
+#[cfg(feature = "olap")]
 #[derive(serde::Serialize, serde::Deserialize)]
 pub struct SinglePurposeToken {
     pub user_id: String,
-    pub purpose: Purpose,
+    pub purpose: TokenPurpose,
+    pub origin: domain::Origin,
     pub exp: u64,
 }
 
-#[derive(Debug, Clone, PartialEq, Eq, strum::Display, serde::Deserialize, serde::Serialize)]
-pub enum Purpose {
-    AcceptInvite,
-}
-
 #[cfg(feature = "olap")]
 impl SinglePurposeToken {
     pub async fn new_token(
         user_id: String,
-        purpose: Purpose,
+        purpose: TokenPurpose,
+        origin: domain::Origin,
         settings: &Settings,
     ) -> UserResult<String> {
         let exp_duration =
@@ -143,6 +144,7 @@ impl SinglePurposeToken {
         let token_payload = Self {
             user_id,
             purpose,
+            origin,
             exp,
         };
         jwt::generate_jwt(&token_payload, settings).await
@@ -308,8 +310,9 @@ where
     }
 }
 
+#[cfg(feature = "olap")]
 #[derive(Debug)]
-pub(crate) struct SinglePurposeJWTAuth(pub Purpose);
+pub(crate) struct SinglePurposeJWTAuth(pub TokenPurpose);
 
 #[cfg(feature = "olap")]
 #[async_trait]
@@ -334,6 +337,7 @@ where
         Ok((
             UserFromSinglePurposeToken {
                 user_id: payload.user_id.clone(),
+                origin: payload.origin.clone(),
             },
             AuthenticationType::SinglePurposeJWT {
                 user_id: payload.user_id,
diff --git a/crates/router/src/services/authentication/blacklist.rs b/crates/router/src/services/authentication/blacklist.rs
index bc11ee19089..c1abb064752 100644
--- a/crates/router/src/services/authentication/blacklist.rs
+++ b/crates/router/src/services/authentication/blacklist.rs
@@ -5,7 +5,9 @@ use common_utils::date_time;
 use error_stack::ResultExt;
 use redis_interface::RedisConnectionPool;
 
-use super::{AuthToken, SinglePurposeToken};
+use super::AuthToken;
+#[cfg(feature = "olap")]
+use super::SinglePurposeToken;
 #[cfg(feature = "email")]
 use crate::consts::{EMAIL_TOKEN_BLACKLIST_PREFIX, EMAIL_TOKEN_TIME_IN_SECS};
 use crate::{
@@ -154,6 +156,7 @@ impl BlackList for AuthToken {
     }
 }
 
+#[cfg(feature = "olap")]
 #[async_trait::async_trait]
 impl BlackList for SinglePurposeToken {
     async fn check_in_blacklist<A>(&self, state: &A) -> RouterResult<bool>
diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs
index fab02df88ba..b4b5df2f46c 100644
--- a/crates/router/src/types/domain/user.rs
+++ b/crates/router/src/types/domain/user.rs
@@ -3,6 +3,7 @@ use std::{collections::HashSet, ops, str::FromStr};
 use api_models::{
     admin as admin_api, organization as api_org, user as user_api, user_role as user_role_api,
 };
+use common_enums::TokenPurpose;
 use common_utils::{errors::CustomResult, pii};
 use diesel_models::{
     enums::UserStatus,
@@ -31,6 +32,8 @@ use crate::{
 };
 
 pub mod dashboard_metadata;
+pub mod decision_manager;
+pub use decision_manager::*;
 
 #[derive(Clone)]
 pub struct UserName(Secret<String>);
@@ -785,6 +788,29 @@ impl UserFromStorage {
             .find_user_role_by_user_id_merchant_id(self.get_user_id(), merchant_id)
             .await
     }
+
+    pub async fn get_preferred_or_active_user_role_from_db(
+        &self,
+        state: &AppState,
+    ) -> CustomResult<UserRole, errors::StorageError> {
+        if let Some(preferred_merchant_id) = self.get_preferred_merchant_id() {
+            self.get_role_from_db_by_merchant_id(state, &preferred_merchant_id)
+                .await
+        } else {
+            state
+                .store
+                .list_user_roles_by_user_id(&self.0.user_id)
+                .await?
+                .into_iter()
+                .find(|role| role.status == UserStatus::Active)
+                .ok_or(
+                    errors::StorageError::ValueNotFound(
+                        "No active role found for user".to_string(),
+                    )
+                    .into(),
+                )
+        }
+    }
 }
 
 impl From<info::ModuleInfo> for user_role_api::ModuleInfo {
@@ -912,7 +938,8 @@ impl SignInWithMultipleRolesStrategy {
                 email: self.user.get_email(),
                 token: auth::SinglePurposeToken::new_token(
                     self.user.get_user_id().to_string(),
-                    auth::Purpose::AcceptInvite,
+                    TokenPurpose::AcceptInvite,
+                    Origin::SignIn,
                     &state.conf,
                 )
                 .await?
diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs
new file mode 100644
index 00000000000..b5aff779106
--- /dev/null
+++ b/crates/router/src/types/domain/user/decision_manager.rs
@@ -0,0 +1,257 @@
+use common_enums::TokenPurpose;
+use diesel_models::{enums::UserStatus, user_role::UserRole};
+use masking::Secret;
+
+use super::UserFromStorage;
+use crate::{
+    core::errors::{UserErrors, UserResult},
+    routes::AppState,
+    services::authentication as auth,
+};
+
+#[derive(Eq, PartialEq, Clone, Copy)]
+pub enum UserFlow {
+    SPTFlow(SPTFlow),
+    JWTFlow(JWTFlow),
+}
+
+impl UserFlow {
+    async fn is_required(&self, user: &UserFromStorage, state: &AppState) -> UserResult<bool> {
+        match self {
+            Self::SPTFlow(flow) => flow.is_required(user, state).await,
+            Self::JWTFlow(flow) => flow.is_required(user, state).await,
+        }
+    }
+}
+
+#[derive(Eq, PartialEq, Clone, Copy)]
+pub enum SPTFlow {
+    TOTP,
+    VerifyEmail,
+    AcceptInvitationFromEmail,
+    ForceSetPassword,
+    MerchantSelect,
+    ResetPassword,
+}
+
+impl SPTFlow {
+    async fn is_required(&self, user: &UserFromStorage, state: &AppState) -> UserResult<bool> {
+        match self {
+            // TOTP
+            Self::TOTP => Ok(true),
+            // Main email APIs
+            Self::AcceptInvitationFromEmail | Self::ResetPassword => Ok(true),
+            Self::VerifyEmail => Ok(user.0.is_verified),
+            // Final Checks
+            // TODO: this should be based on last_password_modified_at as a placeholder using false
+            Self::ForceSetPassword => Ok(false),
+            Self::MerchantSelect => user
+                .get_roles_from_db(state)
+                .await
+                .map(|roles| !roles.iter().any(|role| role.status == UserStatus::Active)),
+        }
+    }
+
+    pub async fn generate_spt(
+        self,
+        state: &AppState,
+        next_flow: &NextFlow,
+    ) -> UserResult<Secret<String>> {
+        auth::SinglePurposeToken::new_token(
+            next_flow.user.get_user_id().to_string(),
+            self.into(),
+            next_flow.origin.clone(),
+            &state.conf,
+        )
+        .await
+        .map(|token| token.into())
+    }
+}
+
+#[derive(Eq, PartialEq, Clone, Copy)]
+pub enum JWTFlow {
+    UserInfo,
+}
+
+impl JWTFlow {
+    async fn is_required(&self, _user: &UserFromStorage, _state: &AppState) -> UserResult<bool> {
+        Ok(true)
+    }
+
+    pub async fn generate_jwt(
+        self,
+        state: &AppState,
+        next_flow: &NextFlow,
+        user_role: &UserRole,
+    ) -> UserResult<Secret<String>> {
+        auth::AuthToken::new_token(
+            next_flow.user.get_user_id().to_string(),
+            user_role.merchant_id.clone(),
+            user_role.role_id.clone(),
+            &state.conf,
+            user_role.org_id.clone(),
+        )
+        .await
+        .map(|token| token.into())
+    }
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum Origin {
+    SignIn,
+    SignUp,
+    MagicLink,
+    VerifyEmail,
+    AcceptInvitationFromEmail,
+    ResetPassword,
+}
+
+impl Origin {
+    fn get_flows(&self) -> &'static [UserFlow] {
+        match self {
+            Self::SignIn => &SIGNIN_FLOW,
+            Self::SignUp => &SIGNUP_FLOW,
+            Self::VerifyEmail => &VERIFY_EMAIL_FLOW,
+            Self::MagicLink => &MAGIC_LINK_FLOW,
+            Self::AcceptInvitationFromEmail => &ACCEPT_INVITATION_FROM_EMAIL_FLOW,
+            Self::ResetPassword => &RESET_PASSWORD_FLOW,
+        }
+    }
+}
+
+const SIGNIN_FLOW: [UserFlow; 4] = [
+    UserFlow::SPTFlow(SPTFlow::TOTP),
+    UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
+    UserFlow::SPTFlow(SPTFlow::MerchantSelect),
+    UserFlow::JWTFlow(JWTFlow::UserInfo),
+];
+
+const SIGNUP_FLOW: [UserFlow; 4] = [
+    UserFlow::SPTFlow(SPTFlow::TOTP),
+    UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
+    UserFlow::SPTFlow(SPTFlow::MerchantSelect),
+    UserFlow::JWTFlow(JWTFlow::UserInfo),
+];
+
+const MAGIC_LINK_FLOW: [UserFlow; 5] = [
+    UserFlow::SPTFlow(SPTFlow::TOTP),
+    UserFlow::SPTFlow(SPTFlow::VerifyEmail),
+    UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
+    UserFlow::SPTFlow(SPTFlow::MerchantSelect),
+    UserFlow::JWTFlow(JWTFlow::UserInfo),
+];
+
+const VERIFY_EMAIL_FLOW: [UserFlow; 5] = [
+    UserFlow::SPTFlow(SPTFlow::TOTP),
+    UserFlow::SPTFlow(SPTFlow::VerifyEmail),
+    UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
+    UserFlow::SPTFlow(SPTFlow::MerchantSelect),
+    UserFlow::JWTFlow(JWTFlow::UserInfo),
+];
+
+const ACCEPT_INVITATION_FROM_EMAIL_FLOW: [UserFlow; 4] = [
+    UserFlow::SPTFlow(SPTFlow::TOTP),
+    UserFlow::SPTFlow(SPTFlow::AcceptInvitationFromEmail),
+    UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
+    UserFlow::JWTFlow(JWTFlow::UserInfo),
+];
+
+const RESET_PASSWORD_FLOW: [UserFlow; 2] = [
+    UserFlow::SPTFlow(SPTFlow::TOTP),
+    UserFlow::SPTFlow(SPTFlow::ResetPassword),
+];
+
+pub struct CurrentFlow {
+    origin: Origin,
+    current_flow_index: usize,
+}
+
+impl CurrentFlow {
+    pub fn new(origin: Origin, current_flow: UserFlow) -> UserResult<Self> {
+        let flows = origin.get_flows();
+        let index = flows
+            .iter()
+            .position(|flow| flow == &current_flow)
+            .ok_or(UserErrors::InternalServerError)?;
+
+        Ok(Self {
+            origin,
+            current_flow_index: index,
+        })
+    }
+
+    pub async fn next(&self, user: UserFromStorage, state: &AppState) -> UserResult<NextFlow> {
+        let flows = self.origin.get_flows();
+        let remaining_flows = flows.iter().skip(self.current_flow_index + 1);
+        for flow in remaining_flows {
+            if flow.is_required(&user, state).await? {
+                return Ok(NextFlow {
+                    origin: self.origin.clone(),
+                    next_flow: *flow,
+                    user,
+                });
+            }
+        }
+        Err(UserErrors::InternalServerError.into())
+    }
+}
+
+pub struct NextFlow {
+    origin: Origin,
+    next_flow: UserFlow,
+    user: UserFromStorage,
+}
+
+impl NextFlow {
+    pub async fn from_origin(
+        origin: Origin,
+        user: UserFromStorage,
+        state: &AppState,
+    ) -> UserResult<Self> {
+        let flows = origin.get_flows();
+        for flow in flows {
+            if flow.is_required(&user, state).await? {
+                return Ok(Self {
+                    origin,
+                    next_flow: *flow,
+                    user,
+                });
+            }
+        }
+        Err(UserErrors::InternalServerError.into())
+    }
+
+    pub fn get_flow(&self) -> UserFlow {
+        self.next_flow
+    }
+}
+
+impl From<UserFlow> for TokenPurpose {
+    fn from(value: UserFlow) -> Self {
+        match value {
+            UserFlow::SPTFlow(flow) => flow.into(),
+            UserFlow::JWTFlow(flow) => flow.into(),
+        }
+    }
+}
+
+impl From<SPTFlow> for TokenPurpose {
+    fn from(value: SPTFlow) -> Self {
+        match value {
+            SPTFlow::TOTP => Self::TOTP,
+            SPTFlow::VerifyEmail => Self::VerifyEmail,
+            SPTFlow::AcceptInvitationFromEmail => Self::AcceptInvitationFromEmail,
+            SPTFlow::MerchantSelect => Self::AcceptInvite,
+            SPTFlow::ResetPassword | SPTFlow::ForceSetPassword => Self::ResetPassword,
+        }
+    }
+}
+
+impl From<JWTFlow> for TokenPurpose {
+    fn from(value: JWTFlow) -> Self {
+        match value {
+            JWTFlow::UserInfo => Self::UserInfo,
+        }
+    }
+}

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

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

## Description
<!-- Describe your changes in detail -->
- This PR will enable APIs to get the next flow from the current flow.
- This PR will also add a query param which will enable to access the new flows.

### 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
<!--
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).
-->
Closes #4517.

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
```
curl --location 'http://localhost:8080/user/v2/signin?token_only=true' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "email",
    "password": "password"
}'
```
Should return with the following response
```
{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMDYxNjE1YjAtZjI5Yi00NWIyLWJmNmItODczNTYyYWU1ODhlIiwicHVycG9zZSI6InRvdHAiLCJvcmlnaW4iOiJTaWduSW4iLCJleHAiOjE3MTQ3MTk2NDh9.GOO-ZM3KDk0yjmNo3y8SgjqCcIYCV96Lj-lhmNl2oOc",
    "token_type": "totp"
}
```

## 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
