kiteconnect_async_wasm/models/auth/
session.rs

1/*!
2Session management data structures for KiteConnect authentication.
3
4Handles login responses, access tokens, and session validation.
5*/
6
7use serde::{Deserialize, Serialize};
8
9/// Response from the `generate_session` API call
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct SessionData {
12    /// The unique, permanent user id registered with the broker and the exchanges
13    pub user_id: String,
14
15    /// User's real name
16    pub user_name: String,
17
18    /// Shortened version of the user's real name
19    pub user_shortname: String,
20
21    /// User's email
22    pub email: String,
23
24    /// User's registered role at the broker. This will be individual for all retail users
25    pub user_type: String,
26
27    /// The broker ID
28    pub broker: String,
29
30    /// Exchanges enabled for trading on the user's account
31    pub exchanges: Vec<String>,
32
33    /// Margin product types enabled for the user
34    pub products: Vec<String>,
35
36    /// Order types enabled for the user
37    pub order_types: Vec<String>,
38
39    /// The API key for which the authentication was performed
40    pub api_key: String,
41
42    /// The authentication token that's used with every subsequent request
43    /// Unless this is invalidated using the API, or invalidated by a master-logout
44    /// from the Kite Web trading terminal, it'll expire at 6 AM on the next day (regulatory requirement)
45    pub access_token: String,
46
47    /// A token for public session validation where requests may be exposed to the public
48    #[serde(default)]
49    pub public_token: String,
50
51    /// A token for getting long standing read permissions. This is only available to certain approved platforms
52    #[serde(default)]
53    pub refresh_token: String,
54
55    /// User's last login time
56    pub login_time: String,
57
58    /// Session metadata containing demat_consent and other user metadata
59    #[serde(default)]
60    pub meta: Option<SessionMeta>,
61
62    /// Full URL to the user's avatar (PNG image) if there's one
63    #[serde(default)]
64    pub avatar_url: Option<String>,
65}
66
67/// Additional session metadata
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct SessionMeta {
70    /// Demat consent status
71    #[serde(default)]
72    pub demat_consent: String,
73}
74
75impl SessionData {
76    /// Check if the session has required authentication data
77    pub fn is_valid(&self) -> bool {
78        !self.access_token.is_empty() && !self.user_id.is_empty()
79    }
80
81    /// Get the access token for API requests
82    pub fn token(&self) -> &str {
83        &self.access_token
84    }
85
86    /// Check if the user has access to a specific exchange
87    pub fn has_exchange(&self, exchange: &str) -> bool {
88        self.exchanges.iter().any(|e| e == exchange)
89    }
90
91    /// Check if the user has access to a specific product
92    pub fn has_product(&self, product: &str) -> bool {
93        self.products.iter().any(|p| p == product)
94    }
95
96    /// Check if the user has access to a specific order type
97    pub fn has_order_type(&self, order_type: &str) -> bool {
98        self.order_types.iter().any(|o| o == order_type)
99    }
100}
101
102/// Login URL configuration for OAuth flow
103#[derive(Debug, Clone)]
104pub struct LoginUrlConfig {
105    /// Base login URL
106    pub base_url: String,
107
108    /// API key
109    pub api_key: String,
110
111    /// Redirect URL after login
112    pub redirect_url: Option<String>,
113
114    /// State parameter for CSRF protection
115    pub state: Option<String>,
116}
117
118impl LoginUrlConfig {
119    /// Create new login URL configuration
120    pub fn new(api_key: impl Into<String>) -> Self {
121        Self {
122            base_url: "https://kite.trade/connect/login".to_string(),
123            api_key: api_key.into(),
124            redirect_url: None,
125            state: None,
126        }
127    }
128
129    /// Set redirect URL
130    pub fn with_redirect_url(mut self, url: impl Into<String>) -> Self {
131        self.redirect_url = Some(url.into());
132        self
133    }
134
135    /// Set state parameter
136    pub fn with_state(mut self, state: impl Into<String>) -> Self {
137        self.state = Some(state.into());
138        self
139    }
140
141    /// Generate the complete login URL
142    pub fn build_url(&self) -> crate::models::common::KiteResult<String> {
143        use url::Url;
144
145        let mut url = Url::parse(&self.base_url)?;
146
147        // Add required parameters
148        url.query_pairs_mut()
149            .append_pair("api_key", &self.api_key)
150            .append_pair("v", "3"); // API version
151
152        // Add optional parameters
153        if let Some(ref redirect_url) = self.redirect_url {
154            url.query_pairs_mut()
155                .append_pair("redirect_url", redirect_url);
156        }
157
158        if let Some(ref state) = self.state {
159            url.query_pairs_mut().append_pair("state", state);
160        }
161
162        Ok(url.to_string())
163    }
164}
165
166/// Request token from OAuth callback
167#[derive(Debug, Clone, Serialize, Deserialize)]
168pub struct RequestToken {
169    /// Request token received from callback
170    pub request_token: String,
171
172    /// State parameter (for CSRF validation)
173    #[serde(default)]
174    pub state: Option<String>,
175
176    /// Action parameter
177    #[serde(default)]
178    pub action: Option<String>,
179
180    /// Status parameter
181    #[serde(default)]
182    pub status: Option<String>,
183}
184
185impl RequestToken {
186    /// Create new request token
187    pub fn new(token: impl Into<String>) -> Self {
188        Self {
189            request_token: token.into(),
190            state: None,
191            action: None,
192            status: None,
193        }
194    }
195
196    /// Validate request token format
197    pub fn is_valid(&self) -> bool {
198        !self.request_token.is_empty() && self.request_token.len() >= 10
199    }
200}
201
202/// Token invalidation response
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct LogoutResponse {
205    /// Success status
206    pub success: bool,
207
208    /// Response message
209    #[serde(default)]
210    pub message: String,
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216
217    #[test]
218    fn test_session_data_validation() {
219        let mut session = SessionData {
220            user_id: "test_user".to_string(),
221            user_name: "Test User".to_string(),
222            user_shortname: "testuser".to_string(),
223            email: "test@example.com".to_string(),
224            user_type: "individual".to_string(),
225            broker: "ZERODHA".to_string(),
226            exchanges: vec!["NSE".to_string(), "BSE".to_string()],
227            products: vec!["CNC".to_string(), "MIS".to_string()],
228            order_types: vec!["MARKET".to_string(), "LIMIT".to_string()],
229            api_key: "test_key".to_string(),
230            access_token: "test_token".to_string(),
231            public_token: String::new(),
232            refresh_token: String::new(),
233            login_time: "2024-01-01 10:00:00".to_string(),
234            meta: None,
235            avatar_url: None,
236        };
237
238        assert!(session.is_valid());
239        assert!(session.has_exchange("NSE"));
240        assert!(!session.has_exchange("MCX"));
241        assert!(session.has_product("CNC"));
242        assert!(session.has_order_type("MARKET"));
243
244        // Test invalid session
245        session.access_token.clear();
246        assert!(!session.is_valid());
247    }
248
249    #[test]
250    fn test_login_url_config() {
251        let config = LoginUrlConfig::new("test_api_key")
252            .with_redirect_url("https://example.com/callback")
253            .with_state("random_state");
254
255        let url = config.build_url().expect("Failed to build URL");
256        println!("Generated URL: {}", url);
257
258        assert!(url.contains("api_key=test_api_key"));
259        assert!(url.contains("v=3"));
260        // The URL encoding might be different, let's check for the unencoded version or partial match
261        assert!(url.contains("redirect_url=") && url.contains("example.com"));
262        assert!(url.contains("state=random_state"));
263    }
264
265    #[test]
266    fn test_request_token_validation() {
267        let valid_token = RequestToken::new("abcdef1234567890");
268        assert!(valid_token.is_valid());
269
270        let invalid_token = RequestToken::new("short");
271        assert!(!invalid_token.is_valid());
272
273        let empty_token = RequestToken::new("");
274        assert!(!empty_token.is_valid());
275    }
276}