Bug: feat: Create new versions of BE pre-login APIs



Currently we've made SPT and decision starter APIs. Now we have to integrate SPT into email specific, signin and signup APIs.

For backwards compatibility, old version should also need to be kept. So we can do these new APIs accessible with a query param.
diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs
index 14676656d0f..b29b28f0136 100644
--- a/crates/api_models/src/events/user.rs
+++ b/crates/api_models/src/events/user.rs
@@ -14,8 +14,8 @@ use crate::user::{
     CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest,
     GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponse,
     InviteUserRequest, ListUsersResponse, ReInviteUserRequest, ResetPasswordRequest,
-    SendVerifyEmailRequest, SignInResponse, SignInWithTokenResponse, SignUpRequest,
-    SignUpWithMerchantIdRequest, SwitchMerchantIdRequest, TokenResponse,
+    SendVerifyEmailRequest, SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest,
+    SwitchMerchantIdRequest, TokenOrPayloadResponse, TokenResponse,
     UpdateUserAccountDetailsRequest, UserFromEmailRequest, UserMerchantCreate, VerifyEmailRequest,
 };
 
@@ -38,6 +38,12 @@ impl ApiEventMetric for VerifyTokenResponse {
     }
 }
 
+impl<T> ApiEventMetric for TokenOrPayloadResponse<T> {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::Miscellaneous)
+    }
+}
+
 common_utils::impl_misc_api_event_type!(
     SignUpRequest,
     SignUpWithMerchantIdRequest,
@@ -62,7 +68,6 @@ common_utils::impl_misc_api_event_type!(
     SignInResponse,
     UpdateUserAccountDetailsRequest,
     GetUserDetailsResponse,
-    SignInWithTokenResponse,
     GetUserRoleDetailsRequest,
     GetUserRoleDetailsResponse,
     TokenResponse,
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index a6d5cf36358..fe4e37f8a8c 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -227,9 +227,9 @@ pub struct TokenResponse {
 
 #[derive(Debug, serde::Serialize)]
 #[serde(untagged)]
-pub enum SignInWithTokenResponse {
+pub enum TokenOrPayloadResponse<T> {
     Token(TokenResponse),
-    SignInResponse(SignInResponse),
+    Payload(T),
 }
 
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs
index 25c1c5ecd40..08d63d9097f 100644
--- a/crates/api_models/src/user_role.rs
+++ b/crates/api_models/src/user_role.rs
@@ -99,6 +99,7 @@ pub enum UserStatus {
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
 pub struct AcceptInvitationRequest {
     pub merchant_ids: Vec<String>,
+    // TODO: Remove this once the token only api is being used
     pub need_dashboard_entry_response: Option<bool>,
 }
 
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 58a6452c9bb..6ad0afa9104 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -94,7 +94,7 @@ pub async fn get_user_details(
 pub async fn signup(
     state: AppState,
     request: user_api::SignUpRequest,
-) -> UserResponse<user_api::SignUpResponse> {
+) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignUpResponse>> {
     let new_user = domain::NewUser::try_from(request)?;
     new_user
         .get_new_merchant()
@@ -117,13 +117,48 @@ pub async fn signup(
     let response =
         utils::user::get_dashboard_entry_response(&state, user_from_db, user_role, token.clone())?;
 
+    auth::cookies::set_cookie_response(user_api::TokenOrPayloadResponse::Payload(response), token)
+}
+
+pub async fn signup_token_only_flow(
+    state: AppState,
+    request: user_api::SignUpRequest,
+) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignUpResponse>> {
+    let new_user = domain::NewUser::try_from(request)?;
+    new_user
+        .get_new_merchant()
+        .get_new_organization()
+        .insert_org_in_db(state.clone())
+        .await?;
+    let user_from_db = new_user
+        .insert_user_and_merchant_in_db(state.clone())
+        .await?;
+    let user_role = new_user
+        .insert_user_role_in_db(
+            state.clone(),
+            consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
+            UserStatus::Active,
+        )
+        .await?;
+
+    let next_flow =
+        domain::NextFlow::from_origin(domain::Origin::SignUp, user_from_db.clone(), &state).await?;
+
+    let token = next_flow
+        .get_token_with_user_role(&state, &user_role)
+        .await?;
+
+    let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse {
+        token: token.clone(),
+        token_type: next_flow.get_flow().into(),
+    });
     auth::cookies::set_cookie_response(response, token)
 }
 
 pub async fn signin(
     state: AppState,
     request: user_api::SignInRequest,
-) -> UserResponse<user_api::SignInWithTokenResponse> {
+) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignInResponse>> {
     let user_from_db: domain::UserFromStorage = state
         .store
         .find_user_by_email(&request.email)
@@ -161,16 +196,13 @@ 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,
-    )
+    auth::cookies::set_cookie_response(user_api::TokenOrPayloadResponse::Payload(response), token)
 }
 
 pub async fn signin_token_only_flow(
     state: AppState,
     request: user_api::SignInRequest,
-) -> UserResponse<user_api::SignInWithTokenResponse> {
+) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignInResponse>> {
     let user_from_db: domain::UserFromStorage = state
         .store
         .find_user_by_email(&request.email)
@@ -185,7 +217,7 @@ pub async fn signin_token_only_flow(
 
     let token = next_flow.get_token(&state).await?;
 
-    let response = user_api::SignInWithTokenResponse::Token(user_api::TokenResponse {
+    let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse {
         token: token.clone(),
         token_type: next_flow.get_flow().into(),
     });
@@ -820,6 +852,73 @@ pub async fn accept_invite_from_email(
     auth::cookies::set_cookie_response(response, token)
 }
 
+#[cfg(feature = "email")]
+pub async fn accept_invite_from_email_token_only_flow(
+    state: AppState,
+    user_token: auth::UserFromSinglePurposeToken,
+    request: user_api::AcceptInviteFromEmailRequest,
+) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::DashboardEntryResponse>> {
+    let token = request.token.expose();
+
+    let email_token = auth::decode_jwt::<email_types::EmailToken>(&token, &state)
+        .await
+        .change_context(UserErrors::LinkInvalid)?;
+
+    auth::blacklist::check_email_token_in_blacklist(&state, &token).await?;
+
+    let user_from_db: domain::UserFromStorage = state
+        .store
+        .find_user_by_email(
+            &email_token
+                .get_email()
+                .change_context(UserErrors::InternalServerError)?,
+        )
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .into();
+
+    if user_from_db.get_user_id() != user_token.user_id {
+        return Err(UserErrors::LinkInvalid.into());
+    }
+
+    let merchant_id = email_token
+        .get_merchant_id()
+        .ok_or(UserErrors::LinkInvalid)?;
+
+    let user_role = state
+        .store
+        .update_user_role_by_user_id_merchant_id(
+            user_from_db.get_user_id(),
+            merchant_id,
+            UserRoleUpdate::UpdateStatus {
+                status: UserStatus::Active,
+                modified_by: user_from_db.get_user_id().to_string(),
+            },
+        )
+        .await
+        .change_context(UserErrors::InternalServerError)?;
+
+    let _ = auth::blacklist::insert_email_token_in_blacklist(&state, &token)
+        .await
+        .map_err(|e| logger::error!(?e));
+
+    let current_flow = domain::CurrentFlow::new(
+        user_token.origin,
+        domain::SPTFlow::AcceptInvitationFromEmail.into(),
+    )?;
+    let next_flow = current_flow.next(user_from_db.clone(), &state).await?;
+
+    let token = next_flow
+        .get_token_with_user_role(&state, &user_role)
+        .await?;
+
+    let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse {
+        token: token.clone(),
+        token_type: next_flow.get_flow().into(),
+    });
+    auth::cookies::set_cookie_response(response, token)
+}
+
 pub async fn create_internal_user(
     state: AppState,
     request: user_api::CreateInternalUserRequest,
@@ -1196,6 +1295,60 @@ pub async fn verify_email(
     auth::cookies::set_cookie_response(response, token)
 }
 
+#[cfg(feature = "email")]
+pub async fn verify_email_token_only_flow(
+    state: AppState,
+    user_token: auth::UserFromSinglePurposeToken,
+    req: user_api::VerifyEmailRequest,
+) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::SignInResponse>> {
+    let token = req.token.clone().expose();
+    let email_token = auth::decode_jwt::<email_types::EmailToken>(&token, &state)
+        .await
+        .change_context(UserErrors::LinkInvalid)?;
+
+    auth::blacklist::check_email_token_in_blacklist(&state, &token).await?;
+
+    let user_from_email = state
+        .store
+        .find_user_by_email(
+            &email_token
+                .get_email()
+                .change_context(UserErrors::InternalServerError)?,
+        )
+        .await
+        .change_context(UserErrors::InternalServerError)?;
+
+    if user_from_email.user_id != user_token.user_id {
+        return Err(UserErrors::LinkInvalid.into());
+    }
+
+    let user_from_db: domain::UserFromStorage = state
+        .store
+        .update_user_by_user_id(
+            user_from_email.user_id.as_str(),
+            storage_user::UserUpdate::VerifyUser,
+        )
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .into();
+
+    let _ = auth::blacklist::insert_email_token_in_blacklist(&state, &token)
+        .await
+        .map_err(|e| logger::error!(?e));
+
+    let current_flow =
+        domain::CurrentFlow::new(user_token.origin, domain::SPTFlow::VerifyEmail.into())?;
+    let next_flow = current_flow.next(user_from_db, &state).await?;
+    let token = next_flow.get_token(&state).await?;
+
+    let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse {
+        token: token.clone(),
+        token_type: next_flow.get_flow().into(),
+    });
+
+    auth::cookies::set_cookie_response(response, token)
+}
+
 #[cfg(feature = "email")]
 pub async fn send_verification_mail(
     state: AppState,
diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs
index 3f913b88a3f..ae20ec51ad4 100644
--- a/crates/router/src/core/user_role.rs
+++ b/crates/router/src/core/user_role.rs
@@ -172,8 +172,7 @@ pub async fn accept_invitation(
     state: AppState,
     user_token: auth::UserFromSinglePurposeToken,
     req: user_role_api::AcceptInvitationRequest,
-    _req_state: ReqState,
-) -> UserResponse<user_api::DashboardEntryResponse> {
+) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::DashboardEntryResponse>> {
     let user_role = futures::future::join_all(req.merchant_ids.iter().map(|merchant_id| async {
         state
             .store
@@ -215,12 +214,65 @@ pub async fn accept_invitation(
             user_role,
             token.clone(),
         )?;
-        return auth::cookies::set_cookie_response(response, token);
+        return auth::cookies::set_cookie_response(
+            user_api::TokenOrPayloadResponse::Payload(response),
+            token,
+        );
     }
 
     Ok(ApplicationResponse::StatusOk)
 }
 
+pub async fn accept_invitation_token_only_flow(
+    state: AppState,
+    user_token: auth::UserFromSinglePurposeToken,
+    req: user_role_api::AcceptInvitationRequest,
+) -> UserResponse<user_api::TokenOrPayloadResponse<user_api::DashboardEntryResponse>> {
+    let user_role = futures::future::join_all(req.merchant_ids.iter().map(|merchant_id| async {
+        state
+            .store
+            .update_user_role_by_user_id_merchant_id(
+                user_token.user_id.as_str(),
+                merchant_id,
+                UserRoleUpdate::UpdateStatus {
+                    status: UserStatus::Active,
+                    modified_by: user_token.user_id.clone(),
+                },
+            )
+            .await
+            .map_err(|e| {
+                logger::error!("Error while accepting invitation {}", e);
+            })
+            .ok()
+    }))
+    .await
+    .into_iter()
+    .reduce(Option::or)
+    .flatten()
+    .ok_or(UserErrors::MerchantIdNotFound)?;
+
+    let user_from_db: domain::UserFromStorage = state
+        .store
+        .find_user_by_id(user_token.user_id.as_str())
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .into();
+
+    let current_flow =
+        domain::CurrentFlow::new(user_token.origin, domain::SPTFlow::MerchantSelect.into())?;
+    let next_flow = current_flow.next(user_from_db.clone(), &state).await?;
+
+    let token = next_flow
+        .get_token_with_user_role(&state, &user_role)
+        .await?;
+
+    let response = user_api::TokenOrPayloadResponse::Token(user_api::TokenResponse {
+        token: token.clone(),
+        token_type: next_flow.get_flow().into(),
+    });
+    auth::cookies::set_cookie_response(response, token)
+}
+
 pub async fn delete_user_role(
     state: AppState,
     user_from_token: auth::UserFromToken,
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index ede6edbceb8..a3f18168501 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -57,15 +57,23 @@ pub async fn user_signup(
     state: web::Data<AppState>,
     http_req: HttpRequest,
     json_payload: web::Json<user_api::SignUpRequest>,
+    query: web::Query<user_api::TokenOnlyQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::UserSignUp;
     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::signup(state, req_body),
+        |state, _, req_body, _| async move {
+            if let Some(true) = is_token_only {
+                user_core::signup_token_only_flow(state, req_body).await
+            } else {
+                user_core::signup(state, req_body).await
+            }
+        },
         &auth::NoAuth,
         api_locking::LockAction::NotApplicable,
     ))
@@ -428,18 +436,37 @@ pub async fn accept_invite_from_email(
     state: web::Data<AppState>,
     req: HttpRequest,
     payload: web::Json<user_api::AcceptInviteFromEmailRequest>,
+    query: web::Query<user_api::TokenOnlyQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::AcceptInviteFromEmail;
-    Box::pin(api::server_wrap(
-        flow,
-        state.clone(),
-        &req,
-        payload.into_inner(),
-        |state, _, request_payload, _| user_core::accept_invite_from_email(state, request_payload),
-        &auth::NoAuth,
-        api_locking::LockAction::NotApplicable,
-    ))
-    .await
+    let is_token_only = query.into_inner().token_only;
+    if let Some(true) = is_token_only {
+        Box::pin(api::server_wrap(
+            flow.clone(),
+            state,
+            &req,
+            payload.into_inner(),
+            |state, user, req_payload, _| {
+                user_core::accept_invite_from_email_token_only_flow(state, user, req_payload)
+            },
+            &auth::SinglePurposeJWTAuth(common_enums::TokenPurpose::AcceptInvitationFromEmail),
+            api_locking::LockAction::NotApplicable,
+        ))
+        .await
+    } else {
+        Box::pin(api::server_wrap(
+            flow,
+            state.clone(),
+            &req,
+            payload.into_inner(),
+            |state, _, request_payload, _| {
+                user_core::accept_invite_from_email(state, request_payload)
+            },
+            &auth::NoAuth,
+            api_locking::LockAction::NotApplicable,
+        ))
+        .await
+    }
 }
 
 #[cfg(feature = "email")]
@@ -447,18 +474,35 @@ pub async fn verify_email(
     state: web::Data<AppState>,
     http_req: HttpRequest,
     json_payload: web::Json<user_api::VerifyEmailRequest>,
+    query: web::Query<user_api::TokenOnlyQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::VerifyEmail;
-    Box::pin(api::server_wrap(
-        flow.clone(),
-        state,
-        &http_req,
-        json_payload.into_inner(),
-        |state, _, req_payload, _| user_core::verify_email(state, req_payload),
-        &auth::NoAuth,
-        api_locking::LockAction::NotApplicable,
-    ))
-    .await
+    let is_token_only = query.into_inner().token_only;
+    if let Some(true) = is_token_only {
+        Box::pin(api::server_wrap(
+            flow.clone(),
+            state,
+            &http_req,
+            json_payload.into_inner(),
+            |state, user, req_payload, _| {
+                user_core::verify_email_token_only_flow(state, user, req_payload)
+            },
+            &auth::SinglePurposeJWTAuth(common_enums::TokenPurpose::VerifyEmail),
+            api_locking::LockAction::NotApplicable,
+        ))
+        .await
+    } else {
+        Box::pin(api::server_wrap(
+            flow.clone(),
+            state,
+            &http_req,
+            json_payload.into_inner(),
+            |state, _, req_payload, _| user_core::verify_email(state, req_payload),
+            &auth::NoAuth,
+            api_locking::LockAction::NotApplicable,
+        ))
+        .await
+    }
 }
 
 #[cfg(feature = "email")]
diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs
index 59c07b79855..d406783cc71 100644
--- a/crates/router/src/routes/user_role.rs
+++ b/crates/router/src/routes/user_role.rs
@@ -1,5 +1,8 @@
 use actix_web::{web, HttpRequest, HttpResponse};
-use api_models::user_role::{self as user_role_api, role as role_api};
+use api_models::{
+    user as user_api,
+    user_role::{self as user_role_api, role as role_api},
+};
 use common_enums::TokenPurpose;
 use router_env::Flow;
 
@@ -206,15 +209,23 @@ pub async fn accept_invitation(
     state: web::Data<AppState>,
     req: HttpRequest,
     json_payload: web::Json<user_role_api::AcceptInvitationRequest>,
+    query: web::Query<user_api::TokenOnlyQueryParam>,
 ) -> HttpResponse {
     let flow = Flow::AcceptInvitation;
     let payload = json_payload.into_inner();
+    let is_token_only = query.into_inner().token_only;
     Box::pin(api::server_wrap(
         flow,
         state.clone(),
         &req,
         payload,
-        user_role_core::accept_invitation,
+        |state, user, req_body, _| async move {
+            if let Some(true) = is_token_only {
+                user_role_core::accept_invitation_token_only_flow(state, user, req_body).await
+            } else {
+                user_role_core::accept_invitation(state, user, req_body).await
+            }
+        },
         &auth::SinglePurposeJWTAuth(TokenPurpose::AcceptInvite),
         api_locking::LockAction::NotApplicable,
     ))
diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs
index e460bc74649..616c595b244 100644
--- a/crates/router/src/types/domain/user/decision_manager.rs
+++ b/crates/router/src/types/domain/user/decision_manager.rs
@@ -7,6 +7,7 @@ use crate::{
     core::errors::{StorageErrorExt, UserErrors, UserResult},
     routes::AppState,
     services::authentication as auth,
+    utils,
 };
 
 #[derive(Eq, PartialEq, Clone, Copy)]
@@ -150,8 +151,9 @@ const VERIFY_EMAIL_FLOW: [UserFlow; 5] = [
     UserFlow::JWTFlow(JWTFlow::UserInfo),
 ];
 
-const ACCEPT_INVITATION_FROM_EMAIL_FLOW: [UserFlow; 4] = [
+const ACCEPT_INVITATION_FROM_EMAIL_FLOW: [UserFlow; 5] = [
     UserFlow::SPTFlow(SPTFlow::TOTP),
+    UserFlow::SPTFlow(SPTFlow::VerifyEmail),
     UserFlow::SPTFlow(SPTFlow::AcceptInvitationFromEmail),
     UserFlow::SPTFlow(SPTFlow::ForceSetPassword),
     UserFlow::JWTFlow(JWTFlow::UserInfo),
@@ -234,16 +236,38 @@ impl NextFlow {
                 {
                     self.user.get_verification_days_left(state)?;
                 }
-
                 let user_role = self
                     .user
                     .get_preferred_or_active_user_role_from_db(state)
                     .await
                     .to_not_found_response(UserErrors::InternalServerError)?;
+                utils::user_role::set_role_permissions_in_cache_by_user_role(state, &user_role)
+                    .await;
+
                 jwt_flow.generate_jwt(state, self, &user_role).await
             }
         }
     }
+
+    pub async fn get_token_with_user_role(
+        &self,
+        state: &AppState,
+        user_role: &UserRole,
+    ) -> UserResult<Secret<String>> {
+        match self.next_flow {
+            UserFlow::SPTFlow(spt_flow) => spt_flow.generate_spt(state, self).await,
+            UserFlow::JWTFlow(jwt_flow) => {
+                #[cfg(feature = "email")]
+                {
+                    self.user.get_verification_days_left(state)?;
+                }
+                utils::user_role::set_role_permissions_in_cache_by_user_role(state, user_role)
+                    .await;
+
+                jwt_flow.generate_jwt(state, self, user_role).await
+            }
+        }
+    }
 }
 
 impl From<UserFlow> for TokenPurpose {
@@ -274,3 +298,15 @@ impl From<JWTFlow> for TokenPurpose {
         }
     }
 }
+
+impl From<SPTFlow> for UserFlow {
+    fn from(value: SPTFlow) -> Self {
+        Self::SPTFlow(value)
+    }
+}
+
+impl From<JWTFlow> for UserFlow {
+    fn from(value: JWTFlow) -> Self {
+        Self::JWTFlow(value)
+    }
+}

## 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 add new core functions for the following APIs which can be used with SPTs.
1. Accept Invite
2. Sign up
3. Verify Email
4. Accept Invite from Email

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

## 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/signup?token_only=true' \
--header 'Content-Type: application/json' \
--data-raw '{
    "email": "user email",
    "password": "password"
}'
```
```
curl --location 'http://localhost:8080/user/accept_invite_from_email?token_only=true' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer SPT with purpose as "accept_invite_from_email"' \
--data '{
    "token": "email token"
}'
```
```
curl --location 'http://localhost:8080/user/v2/verify_email?token_only=true' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer SPT with purpose as "verify_email"' \
--data '{
    "token": "email token"
}'
```
```
curl --location 'http://localhost:8080/user/user/invite/accept?token_only=true' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer SPT with purpose as "accept_invite"' \
--data '{
    "merchant_ids": [
        "merchant_1705414503",
        "merchant_1705414514"
    ],
    "need_dashboard_entry_response": true
}'
```

All API should respond like this
```
{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMDYxNjE1YjAtZjI5Yi00NWIyLWJmNmItODczNTYyYWU1ODhlIiwicHVycG9zZSI6InRvdHAiLCJvcmlnaW4iOiJtYWdpY19saW5rIiwiZXhwIjoxNzE0ODExOTAwfQ.CDErXJ89a1o3ROyAKvm6TFW1drHDnlcqg43OoBQjY6A",
    "token_type": "totp"
}
```
`token_type` can be 
1. `totp`
2. `verify_email`
3. `accept_invite_from_email`
4. `accept_invite`
5. `reset_password`

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