tempie/commands/
setup.rs

1use crate::models::UserCredentials;
2use crate::storage::Storage;
3use dialoguer::{Input, Password};
4
5const JIRA_API_TOKENS_URL: &str = "https://id.atlassian.com/manage-profile/security/api-tokens";
6const TEMPO_API_INTEGRATION_URL: &str =
7    "/plugins/servlet/ac/io.tempo.jira/tempo-app#!/configuration/api-integration";
8
9pub fn setup(storage: &Storage) {
10    if !should_overwrite_credentials(storage) {
11        return;
12    }
13
14    let (jira_url, account_id) = get_jira_credentials();
15    let tempo_token = get_tempo_token(&jira_url);
16    let jira_token = get_jira_token();
17    let jira_email = get_jira_email();
18
19    storage.store_credentials(UserCredentials {
20        url: jira_url.clone(),
21        account_id,
22        tempo_token,
23        jira_token,
24        jira_email,
25    });
26
27    println!("\nUser credentials saved successfully!");
28    println!("{}", format_credentials_for_display(storage));
29}
30
31fn should_overwrite_credentials(storage: &Storage) -> bool {
32    if storage.get_credentials().is_none() {
33        return true;
34    }
35
36    println!("\nUser credentials already saved!");
37    println!("{}", format_credentials_for_display(storage));
38
39    let overwrite_prompt: String = Input::new()
40        .with_prompt("Do you want to overwrite credentials? (y/N)")
41        .default("n".to_string())
42        .interact_text()
43        .unwrap();
44
45    if overwrite_prompt.to_lowercase() == "y" {
46        println!("Overwriting credentials...");
47        true
48    } else {
49        false
50    }
51}
52
53fn format_credentials_for_display(storage: &Storage) -> String {
54    let credentials = storage.get_credentials().unwrap();
55
56    let output = format!(
57        "\nCurrent credentials:
58
59👤 User Email: {}
60🔗 Jira URL: {}
61🔑 Jira Token: {}
62🔑 Tempo Token: {}\n",
63        credentials.jira_email,
64        credentials.url,
65        mask_token(&credentials.jira_token),
66        mask_token(&credentials.tempo_token)
67    );
68
69    output
70}
71
72/// Masks the middle of a token with `***`, keeping a few characters at the start and end.
73fn mask_token(token: &str) -> String {
74    let len = token.len();
75
76    match len {
77        0..=6 => "*".repeat(len), // Fully mask very short tokens
78        _ => {
79            let start_len = len / 4;
80            let end_len = len / 4;
81            let start = &token[..start_len];
82            let end = &token[len - end_len..];
83            format!("{start}***{end}")
84        }
85    }
86}
87
88fn get_jira_credentials() -> (String, String) {
89    println!("\nStep 1/4:");
90    println!("Enter your Jira profile URL to fetch your `account id` and Jira `domain name`:");
91    println!("1. Navigate to the top-right corner and click your avatar");
92    println!("2. Select \"👤 Profile\" from the dropdown menu");
93    println!("3. Copy the URL from your browser's address bar and paste it below:\n");
94
95    let profile_url: String = Input::new()
96        .with_prompt("Enter Jira URL")
97        .interact_text()
98        .unwrap();
99
100    let parts: Vec<&str> = profile_url.split("/jira/people/").collect();
101
102    match parts.as_slice() {
103        [url, id] => {
104            let jira_url = url.to_string();
105            let account_id = id.to_string();
106
107            (jira_url, account_id)
108        }
109        _ => {
110            eprintln!(
111                "\nInvalid Jira URL. Please make sure you've copied the correct profile URL."
112            );
113            eprintln!("Example: https://xxx.jira.com/jira/people/1b2c3d4e5f6g7h8i9j0k");
114            std::process::exit(1);
115        }
116    }
117}
118
119fn get_tempo_token(jira_url: &str) -> String {
120    println!("\nStep 2/4:");
121    println!("Enter your tempo token. You can generate it here:");
122    println!("{}{}\n", jira_url, TEMPO_API_INTEGRATION_URL);
123
124    if webbrowser::open(&format!("{jira_url}{TEMPO_API_INTEGRATION_URL}")).is_ok() {
125        println!("The link should have opened automatically in your browser\n");
126    }
127
128    Password::new()
129        .with_prompt("Enter your Tempo API token")
130        .interact()
131        .unwrap()
132}
133
134fn get_jira_token() -> String {
135    println!("\nStep 3/4:");
136    println!("Enter your Jira API token:");
137    println!("You can generate it here: {}\n", JIRA_API_TOKENS_URL);
138
139    if webbrowser::open(JIRA_API_TOKENS_URL).is_ok() {
140        println!("The link should have opened automatically in your browser\n");
141    }
142
143    Password::new()
144        .with_prompt("Enter your Jira API token")
145        .interact()
146        .unwrap()
147}
148
149fn get_jira_email() -> String {
150    println!("\nStep 4/4:");
151    println!("This is the last step! Enter your Jira email:");
152
153    Input::new()
154        .with_prompt("Enter your Jira email")
155        .interact_text()
156        .unwrap()
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162    use std::fs;
163
164    fn cleanup_test_db(path: &str) {
165        let _ = fs::remove_dir_all(path);
166    }
167
168    #[test]
169    fn test_mask_token() {
170        assert_eq!(mask_token("1234567890"), "12***90");
171        assert_eq!(mask_token("12345678901234567890"), "12345***67890");
172    }
173
174    #[test]
175    fn test_format_credentials_for_display() {
176        let test_db_path = "test_format_credentials_for_display";
177        cleanup_test_db(test_db_path);
178
179        let storage = Storage::with_path(test_db_path);
180        storage.store_credentials(UserCredentials {
181            url: "https://example.com".to_string(),
182            account_id: "1234567890".to_string(),
183            tempo_token: "tempo_token_value".to_string(),
184            jira_token: "jira_token_value".to_string(),
185            jira_email: "example@example.com".to_string(),
186        });
187
188        let output = format_credentials_for_display(&storage);
189
190        assert!(output.contains("👤 User Email: example@example.com"));
191        assert!(output.contains("🔗 Jira URL: https://example.com"));
192        assert!(output.contains("🔑 Jira Token: jira***alue"));
193        assert!(output.contains("🔑 Tempo Token: temp***alue"));
194
195        cleanup_test_db(test_db_path);
196    }
197}