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
72fn mask_token(token: &str) -> String {
74 let len = token.len();
75
76 match len {
77 0..=6 => "*".repeat(len), _ => {
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}