fix: resolve CI workflow path and add lint job
This commit is contained in:
176
src/token.rs
Normal file
176
src/token.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
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()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user