avanza_rs/
client.rs

1use crate::{
2    config::Configuration,
3    error::{self, Error},
4    middleware::ErrorForStatus,
5    totp,
6};
7use serde::{Deserialize, Serialize};
8use std::time::Duration;
9use surf::Client as SurfClient;
10
11#[derive(Debug, Deserialize, Serialize)]
12pub struct Credentials {
13    pub username: String,
14    pub password: String,
15    pub totp_secret: String,
16}
17
18#[derive(Debug, Deserialize, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct TotpBody {
21    totp_code: String,
22    method: String,
23}
24
25#[derive(Debug, Deserialize, Serialize)]
26#[serde(rename_all = "camelCase")]
27pub struct TotpResponse {
28    authentication_session: String,
29    push_subscription_id: String,
30    customer_id: String,
31    registration_complete: bool,
32}
33
34#[derive(Debug, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct SessionUser {
37    pub security_token: String,
38}
39
40#[derive(Debug, Deserialize)]
41#[serde(rename_all = "camelCase")]
42pub struct TwoFactorLogin {
43    pub transaction_id: String,
44    pub method: String,
45}
46
47#[derive(Debug, Deserialize)]
48#[serde(rename_all = "camelCase")]
49pub struct UserCredentialsResponse {
50    pub two_factor_login: TwoFactorLogin,
51}
52
53#[derive(Debug, Deserialize, Serialize)]
54#[serde(rename_all = "camelCase")]
55pub struct UserCredentialsRequest {
56    pub username: String,
57    pub password: String,
58}
59
60pub struct Client {
61    pub(crate) http_client: SurfClient,
62    pub(crate) config: Configuration,
63}
64
65impl Client {
66    pub async fn authenticate(credentials: &Credentials) -> Result<Client, error::Error> {
67        // Create default config.
68        let config = Configuration::default();
69
70        // Send initial auth request with username and password.
71        let user_credentials_response = surf::post(config.urls.authenticate.as_str())
72            .middleware(ErrorForStatus)
73            .body_json(&UserCredentialsRequest {
74                username: credentials.username.clone(),
75                password: credentials.password.clone()
76            })?
77            .recv_json::<UserCredentialsResponse>()
78            .await?;
79
80        // Generate a totp code.
81        let totp_code = totp::generate_totp(&credentials.totp_secret);
82
83        // Send totp code and transaction id as cookie.
84        let mut totp_response = surf::post(config.urls.totp.as_str())
85            .middleware(ErrorForStatus)
86            .header(
87                "cookie",
88                format!(
89                    "AZAMFATRANSACTION={}",
90                    user_credentials_response.two_factor_login.transaction_id
91                ),
92            )
93            .body_json(&TotpBody {
94                method: String::from("TOTP"),
95                totp_code: totp_code.to_string(),
96            })?
97            .await?;
98
99        // Parse the response body.
100        let totp_data = totp_response.body_json::<TotpResponse>().await?;
101
102        let authentication_token = totp_data.authentication_session.clone();
103
104        // Extract security token from headers.
105        let security_token = totp_response
106            .header("x-securitytoken")
107            .ok_or(Error::SecurityTokenError(String::from(
108                "Header 'x-securitytoken was missing in totp request'.",
109            )))?
110            .as_str();
111
112        // Create an http client.
113        let http_client =
114            Self::create_http_client(String::from(security_token), authentication_token.clone())?;
115
116        // Create and return Client.
117        let client = Client {
118            http_client,
119            config: config.clone(),
120        };
121        return Ok(client);
122    }
123
124    fn create_http_client(
125        security_token: String,
126        authentication_token: String,
127    ) -> Result<SurfClient, Error> {
128        let client: SurfClient = surf::Config::new()
129            .set_timeout(Some(Duration::from_secs(5)))
130            .add_header("X-Securitytoken", security_token)?
131            .add_header("X-AuthenticationSession", authentication_token)?
132            .try_into()
133            .map_err(|_| {
134                Error::CreateHTTPClientError(String::from("Could not create http client"))
135            })?;
136        let client = client.with(ErrorForStatus);
137
138        return Ok(client);
139    }
140}