Skip to main content

ig_client/model/
auth.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 19/10/25
5******************************************************************************/
6use crate::application::auth::Session;
7use chrono::Utc;
8use serde::{Deserialize, Serialize};
9use tracing::warn;
10
11/// Response from session creation endpoint
12///
13/// This enum handles both API v2 and v3 session responses using serde's untagged feature
14#[derive(Debug, Clone, Deserialize, Serialize)]
15#[serde(untagged)]
16pub enum SessionResponse {
17    /// API v3 session response with OAuth tokens
18    V3(V3Response),
19    /// API v2 session response with CST/X-SECURITY-TOKEN
20    V2(V2Response),
21}
22
23impl SessionResponse {
24    /// Checks if this is a v3 session response
25    #[must_use]
26    #[inline]
27    pub fn is_v3(&self) -> bool {
28        matches!(self, SessionResponse::V3(_))
29    }
30
31    /// Checks if this is a v2 session response
32    #[must_use]
33    #[inline]
34    pub fn is_v2(&self) -> bool {
35        matches!(self, SessionResponse::V2(_))
36    }
37
38    /// Converts the response to a Session object
39    #[must_use]
40    pub fn get_session(&self) -> Session {
41        match self {
42            SessionResponse::V3(v) => Session {
43                account_id: v.account_id.clone(),
44                client_id: v.client_id.clone(),
45                lightstreamer_endpoint: v.lightstreamer_endpoint.clone(),
46                cst: None,
47                x_security_token: None,
48                oauth_token: Some(v.oauth_token.clone()),
49                api_version: 3,
50                expires_at: v.oauth_token.expire_at(1),
51            },
52            SessionResponse::V2(v) => {
53                let (cst, x_security_token) = match v.security_headers.as_ref() {
54                    Some(headers) => (
55                        Some(headers.cst.clone()),
56                        Some(headers.x_security_token.clone()),
57                    ),
58                    None => (None, None),
59                };
60                let expires_at = (Utc::now().timestamp() + (3600 * 6)) as u64; // 6 hours from now
61                Session {
62                    account_id: v.current_account_id.clone(),
63                    client_id: v.client_id.clone(),
64                    lightstreamer_endpoint: v.lightstreamer_endpoint.clone(),
65                    cst,
66                    x_security_token,
67                    oauth_token: None,
68                    api_version: 2,
69                    expires_at,
70                }
71            }
72        }
73    }
74    /// Converts the response to a Session object using v2 security headers
75    ///
76    /// # Arguments
77    /// * `headers` - Security headers (CST and X-SECURITY-TOKEN)
78    pub fn get_session_v2(&mut self, headers: &SecurityHeaders) -> Session {
79        match self {
80            SessionResponse::V3(_) => {
81                warn!("Returing V3 session from V2 headers - this may be unexpected");
82                self.get_session()
83            }
84            SessionResponse::V2(v) => {
85                v.set_security_headers(headers);
86                v.expires_in = Some(21600); // 6 hours
87                self.get_session()
88            }
89        }
90    }
91
92    /// Checks if the session is expired
93    ///
94    /// # Arguments
95    /// * `margin_seconds` - Safety margin in seconds before actual expiration
96    #[must_use]
97    #[inline]
98    pub fn is_expired(&self, margin_seconds: u64) -> bool {
99        match self {
100            SessionResponse::V3(v) => v.oauth_token.is_expired(margin_seconds),
101            SessionResponse::V2(v) => v.is_expired(margin_seconds),
102        }
103    }
104}
105
106/// API v3 session response
107#[derive(Debug, Clone, Deserialize, Serialize)]
108#[serde(rename_all = "camelCase")]
109pub struct V3Response {
110    /// Client identifier
111    pub client_id: String,
112    /// Account identifier
113    pub account_id: String,
114    /// Timezone offset in minutes
115    pub timezone_offset: i32,
116    /// Lightstreamer WebSocket endpoint URL
117    pub lightstreamer_endpoint: String,
118    /// OAuth token information
119    pub oauth_token: OAuthToken,
120}
121
122/// OAuth token information returned by API v3
123#[derive(serde::Deserialize, serde::Serialize, Debug, Clone)]
124pub struct OAuthToken {
125    /// OAuth access token
126    pub access_token: String,
127    /// OAuth refresh token
128    pub refresh_token: String,
129    /// Token scope
130    pub scope: String,
131    /// Token type (typically "Bearer")
132    pub token_type: String,
133    /// Token expiry time in seconds
134    pub expires_in: String,
135    /// Timestamp when this token was created (for expiry calculation)
136    #[serde(skip, default = "chrono::Utc::now")]
137    pub created_at: chrono::DateTime<Utc>,
138}
139
140impl OAuthToken {
141    /// Checks if the OAuth token is expired or will expire soon
142    ///
143    /// # Arguments
144    /// * `margin_seconds` - Safety margin in seconds before actual expiry
145    ///
146    /// # Returns
147    /// `true` if the token is expired or will expire within the margin, `false` otherwise
148    #[must_use]
149    #[inline]
150    pub fn is_expired(&self, margin_seconds: u64) -> bool {
151        let expires_in_secs = self.expires_in.parse::<i64>().unwrap_or(0);
152        let expiry_time = self.created_at + chrono::Duration::seconds(expires_in_secs);
153        let now = Utc::now();
154        let margin = chrono::Duration::seconds(margin_seconds as i64);
155
156        expiry_time - margin <= now
157    }
158
159    /// Returns the Unix timestamp when the token expires (considering the margin)
160    ///
161    /// # Arguments
162    /// * `margin_seconds` - Safety margin in seconds before actual expiry
163    ///
164    /// # Returns
165    /// Unix timestamp (seconds since epoch) when the token should be considered expired
166    #[must_use]
167    pub fn expire_at(&self, margin_seconds: i64) -> u64 {
168        let expires_in_secs = self.expires_in.parse::<i64>().unwrap_or(0);
169        let expiry_time = self.created_at + chrono::Duration::seconds(expires_in_secs);
170        let margin = chrono::Duration::seconds(margin_seconds);
171
172        // Subtract margin to get the "effective" expiry time
173        let effective_expiry = expiry_time - margin;
174
175        effective_expiry.timestamp() as u64
176    }
177}
178
179/// API v2 session response
180#[derive(Debug, Clone, Deserialize, Serialize)]
181#[serde(rename_all = "camelCase")]
182pub struct V2Response {
183    /// Account type (e.g., "CFD", "SPREADBET")
184    pub account_type: String,
185    /// Account information
186    pub account_info: AccountInfo,
187    /// Currency ISO code (e.g., "GBP", "USD")
188    pub currency_iso_code: String,
189    /// Currency symbol (e.g., "£", "$")
190    pub currency_symbol: String,
191    /// Current active account ID
192    pub current_account_id: String,
193    /// Lightstreamer WebSocket endpoint URL
194    pub lightstreamer_endpoint: String,
195    /// List of all accounts owned by the user
196    pub accounts: Vec<Account>,
197    /// Client identifier
198    pub client_id: String,
199    /// Timezone offset in minutes
200    pub timezone_offset: i32,
201    /// Whether user has active demo accounts
202    pub has_active_demo_accounts: bool,
203    /// Whether user has active live accounts
204    pub has_active_live_accounts: bool,
205    /// Whether trailing stops are enabled
206    pub trailing_stops_enabled: bool,
207    /// Rerouting environment if applicable
208    pub rerouting_environment: Option<String>,
209    /// Whether dealing is enabled
210    pub dealing_enabled: bool,
211    /// Security headers (CST and X-SECURITY-TOKEN)
212    #[serde(skip)]
213    pub security_headers: Option<SecurityHeaders>,
214    /// Token expiry time in seconds
215    #[serde(skip)]
216    pub expires_in: Option<u64>,
217    /// Timestamp when this token was created (for expiry calculation)
218    #[serde(skip, default = "chrono::Utc::now")]
219    pub created_at: chrono::DateTime<Utc>,
220}
221
222impl V2Response {
223    /// Sets the security headers for this session
224    ///
225    /// # Arguments
226    /// * `headers` - Security headers containing CST and X-SECURITY-TOKEN
227    pub fn set_security_headers(&mut self, headers: &SecurityHeaders) {
228        self.security_headers = Some(headers.clone());
229    }
230
231    /// Checks if the session is expired
232    ///
233    /// # Arguments
234    /// * `margin_seconds` - Safety margin in seconds before actual expiration
235    ///
236    /// # Returns
237    /// `true` if the session is expired or `expires_in` was never set
238    #[must_use]
239    pub fn is_expired(&self, margin_seconds: u64) -> bool {
240        match self.expires_in {
241            Some(expires_in) => {
242                let expiry_time = self.created_at + chrono::Duration::seconds(expires_in as i64);
243                let now = Utc::now();
244                let margin = chrono::Duration::seconds(margin_seconds as i64);
245                expiry_time - margin <= now
246            }
247            // If expires_in was never set, treat as expired for safety
248            None => true,
249        }
250    }
251}
252
253/// Security headers for API v2 authentication
254#[derive(Debug, Clone, Deserialize, Serialize)]
255pub struct SecurityHeaders {
256    /// Client Session Token
257    pub cst: String,
258    /// Security token for request authentication
259    pub x_security_token: String,
260    /// API key for the application
261    pub x_ig_api_key: String,
262}
263
264/// Account balance information
265#[derive(Debug, Clone, Deserialize, Serialize)]
266#[serde(rename_all = "camelCase")]
267pub struct AccountInfo {
268    /// Total account balance
269    pub balance: f64,
270    /// Amount deposited
271    pub deposit: f64,
272    /// Current profit or loss
273    pub profit_loss: f64,
274    /// Available funds for trading
275    pub available: f64,
276}
277
278/// Trading account information
279#[derive(Debug, Clone, Deserialize, Serialize)]
280#[serde(rename_all = "camelCase")]
281pub struct Account {
282    /// Unique account identifier
283    pub account_id: String,
284    /// Human-readable account name
285    pub account_name: String,
286    /// Whether this is the preferred/default account
287    pub preferred: bool,
288    /// Account type (e.g., "CFD", "SPREADBET")
289    pub account_type: String,
290}