crates_docs/cli/
list_api_keys_cmd.rs1use crate::config::AppConfig;
4use std::path::Path;
5
6fn truncate_hash_for_display(hash: &str) -> String {
15 const PREFIX_CHARS: usize = 30;
16 const SUFFIX_CHARS: usize = 20;
17 let char_count = hash.chars().count();
18 if char_count > PREFIX_CHARS + SUFFIX_CHARS + 10 {
19 let prefix: String = hash.chars().take(PREFIX_CHARS).collect();
20 let suffix: String = hash.chars().skip(char_count - SUFFIX_CHARS).collect();
21 format!("{prefix}...{suffix}")
22 } else {
23 hash.to_string()
24 }
25}
26
27pub fn run_list_api_keys_command(config_path: &Path) -> Result<(), Box<dyn std::error::Error>> {
36 if !config_path.exists() {
38 eprintln!("Configuration file not found: {}", config_path.display());
39 eprintln!("No API keys are configured.");
40 return Ok(());
41 }
42
43 let config = AppConfig::from_file(config_path).map_err(|e| {
45 format!(
46 "Failed to load configuration from {}: {}",
47 config_path.display(),
48 e
49 )
50 })?;
51
52 println!("API Key Configuration");
53 println!("=====================");
54 println!();
55
56 if !config.auth.api_key.enabled {
57 println!("Status: DISABLED");
58 println!();
59 println!("API key authentication is not enabled.");
60 println!("Set enabled = true in [auth.api_key] section to enable.");
61 return Ok(());
62 }
63
64 println!("Status: ENABLED");
65 println!();
66
67 if config.auth.api_key.keys.is_empty() {
68 println!("No API keys configured.");
69 println!("Use 'crates-docs generate-api-key' to create a new key.");
70 } else {
71 println!("Configured API keys ({}):", config.auth.api_key.keys.len());
72 println!();
73
74 for (index, key_hash) in config.auth.api_key.keys.iter().enumerate() {
75 let key_type = if key_hash.starts_with("legacy:") {
76 "Legacy Hash"
77 } else if key_hash.starts_with("$argon2") {
78 "Argon2 Hash"
79 } else {
80 "Plaintext (Insecure)"
81 };
82
83 println!(" [{}] {}", index + 1, key_type);
84
85 println!(" {}", truncate_hash_for_display(key_hash));
87 println!();
88 }
89
90 println!("Configuration:");
91 println!(" Header name: {}", config.auth.api_key.header_name);
92 println!(" Query param: {}", config.auth.api_key.query_param_name);
93 println!(
94 " Allow query param: {}",
95 config.auth.api_key.allow_query_param
96 );
97 println!(" Key prefix: {}", config.auth.api_key.key_prefix);
98 }
99
100 println!();
101 println!("File: {}", config_path.display());
102
103 Ok(())
104}
105
106#[cfg(test)]
107mod tests {
108 use super::truncate_hash_for_display;
109
110 #[test]
111 fn short_hash_is_unchanged() {
112 let h = "$argon2id$v=19$short";
113 assert_eq!(truncate_hash_for_display(h), h);
114 }
115
116 #[test]
117 fn long_ascii_hash_is_elided() {
118 let h = "a".repeat(80);
119 let out = truncate_hash_for_display(&h);
120 assert_eq!(out, format!("{}...{}", "a".repeat(30), "a".repeat(20)));
121 assert!(out.contains("..."));
122 }
123
124 #[test]
125 fn long_multibyte_hash_does_not_panic_and_stays_valid_utf8() {
126 let h = format!(
129 "{}{}{}",
130 "a".repeat(28),
131 "\u{597d}".repeat(20),
132 "b".repeat(40)
133 );
134 let out = truncate_hash_for_display(&h);
135 assert!(out.contains("..."));
137 assert!(out.starts_with("aaaa"));
138 assert!(out.ends_with("bbbb"));
139 }
140}