gitlab_tracker_redmine/
keyring.rs1use zeroize::Zeroizing;
2
3const KEYRING_SERVICE: &str = "gitlab-tracker-redmine";
5
6fn account_for(redmine_url: &str) -> String {
14 format!("redmine_token::{}", redmine_url.trim_end_matches('/'))
15}
16
17pub fn get_or_prompt_token(redmine_url: &str) -> Option<Zeroizing<String>> {
28 if let Ok(tok) = std::env::var("REDMINE_TOKEN") {
30 let tok = Zeroizing::new(tok.trim().to_string());
31 if !tok.is_empty() {
32 tracing::info!("REDMINE_TOKEN loaded from environment variable");
33 return Some(tok);
34 }
35 }
36
37 let account = account_for(redmine_url);
38
39 match keyring::Entry::new(KEYRING_SERVICE, &account) {
41 Ok(entry) => match entry.get_password() {
42 Ok(pwd) => {
43 let pwd = Zeroizing::new(pwd.trim().to_string());
44 if !pwd.is_empty() {
45 tracing::info!(url = %redmine_url, "REDMINE_TOKEN loaded from OS keyring");
46 return Some(pwd);
47 }
48 tracing::debug!("Redmine keyring entry found but token is empty");
49 }
50 Err(e) => {
51 tracing::debug!(error = %e, "No Redmine token in OS keyring");
52 }
53 },
54 Err(e) => {
55 tracing::warn!(error = %e, "Failed to open Redmine keyring entry");
56 }
57 }
58
59 println!("🔑 No REDMINE_TOKEN found for {redmine_url}.");
61 println!(" Leave empty to disable Redmine integration for this project.");
62 match rpassword::prompt_password("Redmine API token: ") {
63 Ok(raw) => {
64 let token = Zeroizing::new(raw.trim().to_string());
65 if token.is_empty() {
66 tracing::info!("Redmine integration disabled — no token provided");
67 return None;
68 }
69 match keyring::Entry::new(KEYRING_SERVICE, &account) {
71 Ok(entry) => match entry.set_password(&token) {
72 Ok(_) => {
73 tracing::info!(url = %redmine_url, "Redmine token saved to OS keyring");
74 println!("✅ Redmine token securely saved to OS Keyring!\n");
75 }
76 Err(e) => {
77 tracing::error!(error = %e, "Failed to save Redmine token to OS keyring");
78 }
79 },
80 Err(e) => {
81 tracing::error!(error = %e, "Failed to open Redmine keyring entry for writing");
82 }
83 }
84 Some(token)
85 }
86 Err(e) => {
87 tracing::error!(error = %e, "Failed to read Redmine token from prompt");
88 None
89 }
90 }
91}
92
93pub fn delete_token(redmine_url: &str) -> bool {
97 keyring::Entry::new(KEYRING_SERVICE, &account_for(redmine_url))
98 .and_then(|e| e.delete_credential())
99 .is_ok()
100}