ferro_cli/commands/
make_api_key.rs1use console::style;
7use sha2::{Digest, Sha256};
8
9const BASE62: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
10
11pub(crate) struct GeneratedApiKey {
13 pub raw_key: String,
15 pub prefix: String,
17 pub hashed_key: String,
19}
20
21pub(crate) fn generate_api_key(env: &str) -> Option<GeneratedApiKey> {
25 if env != "live" && env != "test" {
26 return None;
27 }
28
29 let prefix_str = format!("fe_{env}_");
30 let mut rng = rand::thread_rng();
31 let random: String = (0..43)
32 .map(|_| {
33 let idx = rand::Rng::gen_range(&mut rng, 0..62);
34 BASE62[idx] as char
35 })
36 .collect();
37
38 let raw_key = format!("{prefix_str}{random}");
39 let prefix = raw_key[..16].to_string();
40
41 let mut hasher = Sha256::new();
42 hasher.update(raw_key.as_bytes());
43 let hashed_key = format!("{:x}", hasher.finalize());
44
45 Some(GeneratedApiKey {
46 raw_key,
47 prefix,
48 hashed_key,
49 })
50}
51
52pub fn run(name: String, env: String) {
54 let key = match generate_api_key(&env) {
55 Some(k) => k,
56 None => {
57 eprintln!(
58 "{} Invalid environment '{}'. Must be 'live' or 'test'.",
59 style("Error:").red().bold(),
60 env
61 );
62 std::process::exit(1);
63 }
64 };
65
66 println!();
67 println!("{}", style("API Key Generated!").green().bold());
68 println!();
69 println!(
70 "{}",
71 style("Raw Key (save this — shown only once):")
72 .yellow()
73 .bold()
74 );
75 println!(" {}", style(&key.raw_key).green());
76 println!();
77 println!("Database values:");
78 println!(" Name: {name}");
79 println!(" Prefix: {}", key.prefix);
80 println!(" Hashed Key: {}", key.hashed_key);
81 println!();
82 println!("Insert SQL:");
83 println!(" INSERT INTO api_keys (name, prefix, hashed_key, created_at)");
84 println!(
85 " VALUES ('{name}', '{}', '{}', datetime('now'));",
86 key.prefix, key.hashed_key
87 );
88 println!();
89 println!("Rust snippet:");
90 println!(" let key = api_keys::ActiveModel {{");
91 println!(" name: Set(\"{name}\".to_string()),");
92 println!(" prefix: Set(\"{}\".to_string()),", key.prefix);
93 println!(" hashed_key: Set(\"{}\".to_string()),", key.hashed_key);
94 println!(" ..Default::default()");
95 println!(" }};");
96 println!(" key.insert(db).await?;");
97 println!();
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn key_format_live() {
106 let key = generate_api_key("live").unwrap();
107 assert!(key.raw_key.starts_with("fe_live_"));
108 assert_eq!(key.raw_key.len(), 51);
110 }
111
112 #[test]
113 fn key_format_test() {
114 let key = generate_api_key("test").unwrap();
115 assert!(key.raw_key.starts_with("fe_test_"));
116 assert_eq!(key.raw_key.len(), 51);
117 }
118
119 #[test]
120 fn prefix_is_first_16_chars() {
121 let key = generate_api_key("live").unwrap();
122 assert_eq!(key.prefix.len(), 16);
123 assert_eq!(key.prefix, &key.raw_key[..16]);
124 }
125
126 #[test]
127 fn hash_is_valid_sha256_hex() {
128 let key = generate_api_key("live").unwrap();
129 assert_eq!(key.hashed_key.len(), 64);
130 assert!(key.hashed_key.chars().all(|c| c.is_ascii_hexdigit()));
131 }
132
133 #[test]
134 fn hash_matches_raw_key() {
135 let key = generate_api_key("live").unwrap();
136 let mut hasher = Sha256::new();
137 hasher.update(key.raw_key.as_bytes());
138 let expected = format!("{:x}", hasher.finalize());
139 assert_eq!(key.hashed_key, expected);
140 }
141
142 #[test]
143 fn consecutive_keys_are_unique() {
144 let key1 = generate_api_key("live").unwrap();
145 let key2 = generate_api_key("live").unwrap();
146 assert_ne!(key1.raw_key, key2.raw_key);
147 assert_ne!(key1.hashed_key, key2.hashed_key);
148 }
149
150 #[test]
151 fn random_part_is_base62() {
152 let key = generate_api_key("live").unwrap();
153 let random_part = &key.raw_key[8..]; assert!(random_part.chars().all(|c| c.is_ascii_alphanumeric()));
155 }
156
157 #[test]
158 fn invalid_env_returns_none() {
159 assert!(generate_api_key("staging").is_none());
160 assert!(generate_api_key("production").is_none());
161 assert!(generate_api_key("").is_none());
162 }
163}