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().append_pair("redirect_url", redirect_url);
155        }
156        
157        if let Some(ref state) = self.state {
158            url.query_pairs_mut().append_pair("state", state);
159        }
160        
161        Ok(url.to_string())
162    }
163}
164
165/// Request token from OAuth callback
166#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct RequestToken {
168    /// Request token received from callback
169    pub request_token: String,
170    
171    /// State parameter (for CSRF validation)
172    #[serde(default)]
173    pub state: Option<String>,
174    
175    /// Action parameter
176    #[serde(default)]
177    pub action: Option<String>,
178    
179    /// Status parameter
180    #[serde(default)]
181    pub status: Option<String>,
182}
183
184impl RequestToken {
185    /// Create new request token
186    pub fn new(token: impl Into<String>) -> Self {
187        Self {
188            request_token: token.into(),
189            state: None,
190            action: None,
191            status: None,
192        }
193    }
194    
195    /// Validate request token format
196    pub fn is_valid(&self) -> bool {
197        !self.request_token.is_empty() && self.request_token.len() >= 10
198    }
199}
200
201/// Token invalidation response
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct LogoutResponse {
204    /// Success status
205    pub success: bool,
206    
207    /// Response message
208    #[serde(default)]
209    pub message: String,
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215    
216    #[test]
217    fn test_session_data_validation() {
218        let mut session = SessionData {
219            user_id: "test_user".to_string(),
220            user_name: "Test User".to_string(),
221            user_shortname: "testuser".to_string(),
222            email: "test@example.com".to_string(),
223            user_type: "individual".to_string(),
224            broker: "ZERODHA".to_string(),
225            exchanges: vec!["NSE".to_string(), "BSE".to_string()],
226            products: vec!["CNC".to_string(), "MIS".to_string()],
227            order_types: vec!["MARKET".to_string(), "LIMIT".to_string()],
228            api_key: "test_key".to_string(),
229            access_token: "test_token".to_string(),
230            public_token: String::new(),
231            refresh_token: String::new(),
232            login_time: "2024-01-01 10:00:00".to_string(),
233            meta: None,
234            avatar_url: None,
235        };
236        
237        assert!(session.is_valid());
238        assert!(session.has_exchange("NSE"));
239        assert!(!session.has_exchange("MCX"));
240        assert!(session.has_product("CNC"));
241        assert!(session.has_order_type("MARKET"));
242        
243        // Test invalid session
244        session.access_token.clear();
245        assert!(!session.is_valid());
246    }
247    
248    #[test]
249    fn test_login_url_config() {
250        let config = LoginUrlConfig::new("test_api_key")
251            .with_redirect_url("https://example.com/callback")
252            .with_state("random_state");
253        
254        let url = config.build_url().expect("Failed to build URL");
255        println!("Generated URL: {}", url);
256        
257        assert!(url.contains("api_key=test_api_key"));
258        assert!(url.contains("v=3"));
259        // The URL encoding might be different, let's check for the unencoded version or partial match
260        assert!(url.contains("redirect_url=") && url.contains("example.com"));
261        assert!(url.contains("state=random_state"));
262    }
263    
264    #[test]
265    fn test_request_token_validation() {
266        let valid_token = RequestToken::new("abcdef1234567890");
267        assert!(valid_token.is_valid());
268        
269        let invalid_token = RequestToken::new("short");
270        assert!(!invalid_token.is_valid());
271        
272        let empty_token = RequestToken::new("");
273        assert!(!empty_token.is_valid());
274    }
275}