Skip to main content

kasl/api/
mod.rs

1//! External API clients (GitLab, Jira, SiServer) and the shared
2//! session-management pattern they implement.
3//!
4//! ```text
5//! use kasl::api::{GitLabConfig, JiraConfig, SiConfig};
6//!
7//! let jira_module = JiraConfig::module();
8//! let jira_config = JiraConfig::init(&existing_config)?;
9//! ```
10
11use crate::libs::messages::Message;
12use crate::libs::{data_storage::DataStorage, secret::Secret};
13use crate::msg_error_anyhow;
14use anyhow::Result;
15use std::fs;
16use std::io::Write;
17
18pub mod gitlab;
19pub mod jira;
20pub mod si;
21
22pub use gitlab::GitLabConfig;
23pub use jira::JiraConfig;
24pub use si::SiConfig;
25
26/// Authentication attempts before giving up on a password.
27const MAX_RETRY_COUNT: i32 = 3;
28
29/// Session lifecycle shared by every API client: cache the session id on
30/// disk, prompt for the password only when needed, retry a bounded number
31/// of times.
32///
33/// Implementors supply the provider-specific pieces (`login`,
34/// `set_credentials`, file names); the provided methods do the rest.
35#[allow(async_fn_in_trait)]
36pub trait Session {
37    /// Authenticates with the stored credentials, returning a session id.
38    async fn login(&self) -> Result<String>;
39
40    /// Stores the password (encoded as the provider requires) for `login`.
41    fn set_credentials(&mut self, password: &str) -> Result<()>;
42
43    /// Per-provider session cache filename.
44    fn session_id_file(&self) -> &str;
45
46    /// Per-provider secret manager (prompt text, cache file).
47    fn secret(&self) -> Secret;
48
49    /// Current failed-attempt count.
50    fn retry(&self) -> i32;
51
52    /// Bumps the failed-attempt count.
53    fn inc_retry(&mut self);
54
55    /// Clears the failed-attempt count after a successful login.
56    fn reset_retry(&mut self);
57
58    /// Returns a session id: cached if available, otherwise via login with
59    /// up to [`MAX_RETRY_COUNT`] password attempts (a retry always
60    /// re-prompts rather than reusing a password that just failed).
61    async fn get_session_id(&mut self) -> Result<String> {
62        let session_id_file_path = DataStorage::new().get_path(self.session_id_file())?;
63        let session_id_file_path_str = session_id_file_path.to_str().unwrap();
64
65        if let Ok(session_id) = Self::read_session_id(session_id_file_path_str) {
66            Ok(session_id)
67        } else {
68            loop {
69                let password: String = match self.retry() > 0 {
70                    true => self.secret().prompt()?,         // Force new prompt on retry
71                    false => self.secret().get_or_prompt()?, // Use cache if available
72                };
73
74                self.set_credentials(&password)?;
75
76                let session_id = self.login().await;
77                match session_id {
78                    Ok(session_id) => {
79                        let _ = Self::write_session_id(session_id_file_path_str, &session_id);
80                        self.reset_retry();
81                        return Ok(session_id);
82                    }
83                    Err(_) => {
84                        if self.retry() < MAX_RETRY_COUNT {
85                            self.inc_retry();
86                            continue;
87                        }
88                        break Err(msg_error_anyhow!(Message::WrongPassword(MAX_RETRY_COUNT)));
89                    }
90                }
91            }
92        }
93    }
94
95    /// Reads the cached session id.
96    fn read_session_id(file_name: &str) -> Result<String> {
97        Ok(fs::read_to_string(file_name)?)
98    }
99
100    /// Writes the session id to the cache file.
101    fn write_session_id(file_name: &str, session_id: &str) -> Result<()> {
102        let mut file = fs::OpenOptions::new().write(true).create(true).truncate(true).open(file_name)?;
103        file.write_all(session_id.as_bytes())?;
104        Ok(())
105    }
106
107    /// Drops the cached session, forcing a fresh login next time.
108    fn delete_session_id(&self) -> Result<()> {
109        let session_id_file_path = DataStorage::new().get_path(self.session_id_file())?;
110        fs::remove_file(session_id_file_path)?;
111        Ok(())
112    }
113}