Files
api-token-vault/src/token.rs
7000pctAUTO 0fbb5d7418
Some checks failed
CI / test (push) Has been cancelled
CI / lint (push) Has been cancelled
fix: resolve CI workflow path and add lint job
2026-01-31 23:08:35 +00:00

177 lines
5.0 KiB
Rust

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<Utc>,
pub expires_at: Option<DateTime<Utc>>,
pub auto_rotate: bool,
pub rotation_days: Option<u32>,
pub last_rotated: Option<DateTime<Utc>>,
pub metadata: Option<TokenMetadata>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct TokenMetadata {
pub description: Option<String>,
pub service: Option<String>,
pub tags: Vec<String>,
}
impl TokenData {
pub fn new(value: String, rotation_days: Option<u32>) -> 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<usize>, 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::<uuid::Uuid>().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()));
}
}