Skip to main content

kasl/api/
mod.rs

1//! API client modules for external service integrations.
2//!
3//! Provides a unified interface for interacting with various external APIs
4//! that kasl integrates with. Includes clients for GitLab, Jira, and internal
5//! SiServer systems, all implementing a common session management pattern.
6//!
7//! ## Features
8//!
9//! - **GitLab**: Fetches user activity and commit data for task creation
10//! - **Jira**: Retrieves assigned issues and project information
11//! - **SiServer**: Internal reporting API for time tracking submissions
12//! - **Session Management**: Automatic caching, encrypted storage, retry logic
13//! - **Security**: Encrypted tokens, secure prompting, session invalidation
14//!
15//! ## Usage
16//!
17//! ```text
18//! use kasl::api::{GitLabConfig, JiraConfig, SiConfig};
19//!
20//! let jira_module = JiraConfig::module();
21//! let jira_config = JiraConfig::init(&existing_config)?;
22//! ```
23
24use crate::libs::messages::Message;
25use crate::libs::{data_storage::DataStorage, secret::Secret};
26use crate::msg_error_anyhow;
27use anyhow::Result;
28use std::fs;
29use std::io::Write;
30
31// API client modules
32pub mod gitlab;
33pub mod jira;
34pub mod si;
35
36// Re-export configuration structs for easier access from other modules
37pub use gitlab::GitLabConfig;
38pub use jira::JiraConfig;
39pub use si::SiConfig;
40
41/// Maximum number of authentication retry attempts before giving up.
42///
43/// This prevents infinite loops when credentials are consistently invalid
44/// and provides a reasonable number of attempts for user input errors.
45const MAX_RETRY_COUNT: i32 = 3;
46
47/// Common session management trait for all API clients.
48///
49/// Provides a standardized interface for handling authentication, session caching,
50/// and credential management across different API providers.
51#[allow(async_fn_in_trait)]
52pub trait Session {
53    /// Performs authentication and returns a session identifier.
54    ///
55    /// This method handles the actual API authentication process using stored
56    /// credentials. The returned session ID can be used for subsequent API calls.
57    ///
58    /// # Returns
59    ///
60    /// * `Result<String>` - Session identifier on success, error on failure
61    ///
62    /// # Errors
63    ///
64    /// Returns an error if:
65    /// - Network connection fails
66    /// - Credentials are invalid
67    /// - API returns an unexpected response format
68    async fn login(&self) -> Result<String>;
69
70    /// Sets user credentials for authentication.
71    ///
72    /// Stores the provided password in memory for use during authentication.
73    /// The password may be encoded or hashed depending on the API requirements.
74    ///
75    /// # Arguments
76    ///
77    /// * `password` - User password in plain text
78    ///
79    /// # Errors
80    ///
81    /// Returns an error if password encoding/validation fails.
82    fn set_credentials(&mut self, password: &str) -> Result<()>;
83
84    /// Returns the filename used for session storage.
85    ///
86    /// Each API client uses a unique session file to avoid conflicts.
87    /// Files are stored in the application's data directory with restricted permissions.
88    fn session_id_file(&self) -> &str;
89
90    /// Returns the secret manager for this API client.
91    ///
92    /// Provides access to encrypted credential storage and interactive prompting
93    /// specific to this API provider.
94    fn secret(&self) -> Secret;
95
96    /// Returns current retry attempt count.
97    ///
98    /// Used to track authentication failures and implement retry limits.
99    fn retry(&self) -> i32;
100
101    /// Increments the retry counter.
102    ///
103    /// Called after each failed authentication attempt to track progress
104    /// toward the maximum retry limit.
105    fn inc_retry(&mut self);
106
107    /// Resets the retry counter to zero.
108    ///
109    /// Called after successful authentication to ensure future sessions
110    /// don't inherit retry state from previous attempts.
111    fn reset_retry(&mut self);
112
113    /// Retrieves or establishes a valid session ID.
114    ///
115    /// This is the main entry point for session management. It handles the complete
116    /// session lifecycle including cache restoration, authentication, and retry logic.
117    ///
118    /// ## Process Flow
119    ///
120    /// 1. **Cache Check**: Attempt to restore session from encrypted storage
121    /// 2. **Authentication Loop**: If no cache, prompt for credentials and authenticate
122    /// 3. **Retry Logic**: Handle failures with limited retry attempts
123    /// 4. **Session Storage**: Cache successful sessions for future use
124    ///
125    /// # Returns
126    ///
127    /// * `Result<String>` - Valid session ID ready for API calls
128    ///
129    /// # Errors
130    ///
131    /// Returns an error if:
132    /// - Maximum retry attempts exceeded
133    /// - Storage operations fail
134    /// - Network or API errors prevent authentication
135    async fn get_session_id(&mut self) -> Result<String> {
136        // Attempt to restore session from encrypted cache
137        let session_id_file_path = DataStorage::new().get_path(self.session_id_file())?;
138        let session_id_file_path_str = session_id_file_path.to_str().unwrap();
139
140        if let Ok(session_id) = Self::read_session_id(session_id_file_path_str) {
141            Ok(session_id)
142        } else {
143            // No valid cached session - begin authentication process
144            loop {
145                // Get password from cache or interactive prompt
146                let password: String = match self.retry() > 0 {
147                    true => self.secret().prompt()?,         // Force new prompt on retry
148                    false => self.secret().get_or_prompt()?, // Use cache if available
149                };
150
151                // Set credentials for authentication
152                self.set_credentials(&password)?;
153
154                // Attempt to authenticate with the API
155                let session_id = self.login().await;
156                match session_id {
157                    Ok(session_id) => {
158                        // Success - cache the session and reset retry counter
159                        let _ = Self::write_session_id(session_id_file_path_str, &session_id);
160                        self.reset_retry(); // Reset retry counter after successful authentication
161                        return Ok(session_id);
162                    }
163                    Err(_) => {
164                        // Authentication failed - check retry limit
165                        if self.retry() < MAX_RETRY_COUNT {
166                            self.inc_retry();
167                            continue; // Try again with new credentials
168                        }
169                        // Maximum retries exceeded
170                        break Err(msg_error_anyhow!(Message::WrongPassword(MAX_RETRY_COUNT)));
171                    }
172                }
173            }
174        }
175    }
176
177    /// Reads a session ID from the specified file.
178    ///
179    /// Attempts to load a cached session identifier from disk storage.
180    /// The session may be encrypted depending on the implementation.
181    ///
182    /// # Arguments
183    ///
184    /// * `file_name` - Path to the session storage file
185    ///
186    /// # Returns
187    ///
188    /// * `Result<String>` - Session ID if file exists and is readable
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if the file doesn't exist, is unreadable, or contains
193    /// invalid session data.
194    fn read_session_id(file_name: &str) -> Result<String> {
195        Ok(fs::read_to_string(file_name)?)
196    }
197
198    /// Writes a session ID to the specified file.
199    ///
200    /// Stores the session identifier for future use, potentially with encryption.
201    /// The file is created with restricted permissions for security.
202    ///
203    /// # Arguments
204    ///
205    /// * `file_name` - Path where session should be stored
206    /// * `session_id` - Session identifier to save
207    ///
208    /// # Returns
209    ///
210    /// * `Result<()>` - Success indicator
211    ///
212    /// # Errors
213    ///
214    /// Returns an error if file creation or writing fails.
215    fn write_session_id(file_name: &str, session_id: &str) -> Result<()> {
216        let mut file = fs::OpenOptions::new().write(true).create(true).truncate(true).open(file_name)?;
217        file.write_all(session_id.as_bytes())?;
218        Ok(())
219    }
220
221    /// Deletes the cached session file.
222    ///
223    /// Removes the session cache when authentication fails or sessions expire.
224    /// This forces fresh authentication on the next session request.
225    ///
226    /// # Returns
227    ///
228    /// * `Result<()>` - Success indicator
229    ///
230    /// # Errors
231    ///
232    /// Returns an error if file deletion fails. Missing files are not considered errors.
233    fn delete_session_id(&self) -> Result<()> {
234        let session_id_file_path = DataStorage::new().get_path(self.session_id_file())?;
235        fs::remove_file(session_id_file_path)?;
236        Ok(())
237    }
238}