1use reqwest::header::{ACCEPT, CONTENT_LENGTH};
2use reqwest::Client;
3use serde::Deserialize;
4use std::error::Error;
5use std::ops::Add;
6use std::time::{Duration, Instant};
7
8#[derive(Clone, PartialEq, Debug)]
10pub struct AuthenticationInfo {
11 pub refresh_token: String,
13
14 pub access_token: String,
16
17 pub expires_at: Instant,
19
20 pub api_server: String,
22
23 pub is_demo: bool,
25}
26
27impl AuthenticationInfo {
28 pub async fn authenticate(
30 refresh_token: &str,
31 is_demo: bool,
32 client: &Client,
33 ) -> Result<AuthenticationInfo, Box<dyn Error>> {
34 Self::refresh_access_token(refresh_token, is_demo, client).await
35 }
36
37 async fn refresh(&self, client: &Client) -> Result<AuthenticationInfo, Box<dyn Error>> {
38 Self::refresh_access_token(self.refresh_token.as_str(), self.is_demo, client).await
39 }
40
41 async fn refresh_access_token(
42 refresh_token: &str,
43 is_demo: bool,
44 client: &Client,
45 ) -> Result<AuthenticationInfo, Box<dyn Error>> {
46 #[derive(Deserialize, Clone, PartialEq, Debug)]
47 pub struct AuthenticationInfoResponse {
48 pub refresh_token: String,
49 pub access_token: String,
50 pub expires_in: u64,
51 pub api_server: String,
52 }
53
54 let url = get_url(is_demo);
55
56 let response = client
57 .post(url)
58 .query(&[
59 ("grant_type", "refresh_token"),
60 ("refresh_token", refresh_token),
61 ])
62 .header(CONTENT_LENGTH, 0)
63 .header(ACCEPT, "application/json")
64 .send()
65 .await?
66 .error_for_status()?
67 .json::<AuthenticationInfoResponse>()
68 .await?;
69
70 Ok(AuthenticationInfo {
71 refresh_token: response.refresh_token,
72 access_token: response.access_token,
73 expires_at: Instant::now().add(Duration::from_secs(response.expires_in)),
74 api_server: response.api_server.trim_end_matches('/').into(),
75 is_demo,
76 })
77 }
78}
79
80#[inline]
82fn get_url(is_demo: bool) -> &'static str {
83 if is_demo {
84 "https://practicelogin.questrade.com/oauth2/token"
85 } else {
86 "https://login.questrade.com/oauth2/token"
87 }
88}