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}