use chrono::{DateTime, Utc}; use rand::Rng; use uuid::Uuid; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TokenData { pub value: String, pub created_at: DateTime, pub expires_at: Option>, pub auto_rotate: bool, pub rotation_days: Option, pub last_rotated: Option>, pub metadata: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct TokenMetadata { pub description: Option, pub service: Option, pub tags: Vec, } impl TokenData { pub fn new(value: String, rotation_days: Option) -> Self { let created_at = Utc::now(); let expires_at = rotation_days.map(|days| created_at + chrono::Duration::days(days as i64)); TokenData { value, created_at, expires_at, auto_rotate: rotation_days.is_some(), rotation_days, last_rotated: None, metadata: None, } } pub fn is_expired(&self) -> bool { if let Some(expires_at) = self.expires_at { Utc::now() > expires_at } else { false } } pub fn should_rotate(&self) -> bool { self.auto_rotate && self.is_expired() } } pub struct TokenGenerator; impl TokenGenerator { const CHARSET_ALPHA: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; const CHARSET_ALPHANUMERIC: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const CHARSET_BASE64: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; pub fn generate(length: usize, charset: TokenCharset) -> String { let charset_bytes = match charset { TokenCharset::Alpha => Self::CHARSET_ALPHA, TokenCharset::Alphanumeric => Self::CHARSET_ALPHANUMERIC, TokenCharset::Base64 => Self::CHARSET_BASE64, TokenCharset::Hex => b"0123456789abcdef", }; let mut rng = rand::thread_rng(); let chars_len = charset_bytes.len(); (0..length) .map(|_| { let idx = rng.gen_range(0..chars_len); charset_bytes[idx] as char }) .collect() } pub fn generate_secure(length: usize) -> String { let mut bytes = vec![0u8; length]; rand::Rng::fill(&mut rand::thread_rng(), &mut bytes); base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes) .trim_end_matches('=') .to_string() } pub fn generate_uuid() -> String { Uuid::new_v4().to_string() } pub fn generate_api_key(prefix: &str, length: usize) -> String { let token = Self::generate_secure(length); format!("{}_{}", prefix, token) } pub fn generate_hex(length: usize) -> String { Self::generate(length, TokenCharset::Hex) } } #[derive(Debug, Clone, Copy)] pub enum TokenCharset { Alpha, Alphanumeric, Base64, Hex, } pub fn generate_token_value(length: Option, include_special: bool) -> String { let length = length.unwrap_or(32); if include_special { generate_secure_token_with_special(length) } else { TokenGenerator::generate_secure(length) } } fn generate_secure_token_with_special(length: usize) -> String { let special_chars = b"!@#$%^&*()_+-=[]{}|;:,.<>?"; let base_chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let mut rng = rand::thread_rng(); let mut result = String::with_capacity(length); for i in 0..length { if i % 4 == 3 && length - i > 2 { let idx = rng.gen_range(0..special_chars.len()); result.push(special_chars[idx] as char); } else { let idx = rng.gen_range(0..base_chars.len()); result.push(base_chars[idx] as char); } } result } #[cfg(test)] mod tests { use super::*; #[test] fn test_token_generation() { let token = TokenGenerator::generate(32, TokenCharset::Alphanumeric); assert_eq!(token.len(), 32); } #[test] fn test_secure_token_generation() { let token = TokenGenerator::generate_secure(32); assert!(!token.is_empty()); assert!(token.len() >= 32); } #[test] fn test_uuid_generation() { let uuid1 = TokenGenerator::generate_uuid(); let uuid2 = TokenGenerator::generate_uuid(); assert_ne!(uuid1, uuid2); assert!(uuid1.parse::().is_ok()); } #[test] fn test_token_data_expiration() { let token_data = TokenData::new("test".to_string(), Some(30)); assert!(!token_data.is_expired()); } #[test] fn test_generate_hex() { let hex = TokenGenerator::generate_hex(32); assert_eq!(hex.len(), 32); assert!(hex.chars().all(|c| c.is_ascii_hexdigit())); } }