veza/veza-common/src/auth.rs
senke ad60247f33 feat: global update including storybook setup and backend fixes
- Web: Setup Storybook, added addons, configured Tailwind, added stories for UI components.
- Backend: Updated API router, database, workers, and auth in common.
- Stream Server: Removed SQLx queries and updated auth.
- Docs & Scripts: Updated documentation and recovery scripts.
2026-02-02 19:34:14 +01:00

371 lines
9.8 KiB
Rust

//! Authentication utilities for Veza Rust services
//!
//! This module provides authentication-related utilities and helpers.
use uuid::Uuid;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::{VezaError, VezaResult};
/// JWT claims structure
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JwtClaims {
pub sub: Uuid, // Subject (user ID)
pub iss: String, // Issuer
pub aud: String, // Audience
pub exp: u64, // Expiration time
pub iat: u64, // Issued at
pub nbf: Option<u64>, // Not before
pub jti: Option<String>, // JWT ID
pub username: String,
pub email: String,
pub roles: Vec<String>,
}
impl JwtClaims {
/// Create new JWT claims
pub fn new(
user_id: Uuid,
username: String,
email: String,
roles: Vec<String>,
issuer: String,
audience: String,
ttl_seconds: u64,
) -> Self {
let now = Utc::now().timestamp() as u64;
Self {
sub: user_id,
iss: issuer,
aud: audience,
exp: now + ttl_seconds,
iat: now,
nbf: Some(now),
jti: Some(uuid::Uuid::new_v4().to_string()),
username,
email,
roles,
}
}
/// Check if claims are expired
pub fn is_expired(&self) -> bool {
let now = Utc::now().timestamp() as u64;
self.exp < now
}
/// Check if claims are valid (not before)
pub fn is_valid(&self) -> bool {
let now = Utc::now().timestamp() as u64;
if let Some(nbf) = self.nbf {
now >= nbf
} else {
true
}
}
/// Check if user has role
pub fn has_role(&self, role: &str) -> bool {
self.roles.contains(&role.to_string())
}
/// Check if user has any of the roles
pub fn has_any_role(&self, roles: &[&str]) -> bool {
roles.iter().any(|role| self.has_role(role))
}
}
/// Authentication result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthResult {
pub access_token: String,
pub refresh_token: String,
pub token_type: String,
pub expires_in: u64,
pub user_id: Uuid,
pub username: String,
pub email: String,
pub roles: Vec<String>,
}
/// Password validation result
#[derive(Debug, Clone)]
pub struct PasswordValidation {
pub is_valid: bool,
pub errors: Vec<String>,
pub strength_score: u8, // 0-100
}
impl PasswordValidation {
/// Create new password validation
pub fn new(password: &str) -> Self {
let mut errors = Vec::new();
let mut strength_score = 0u8;
// Length check
if password.len() >= 8 {
strength_score += 20;
} else {
errors.push("Password must be at least 8 characters long".to_string());
}
if password.len() >= 12 {
strength_score += 10;
}
// Character variety checks
if password.chars().any(|c| c.is_uppercase()) {
strength_score += 20;
} else {
errors.push("Password must contain at least one uppercase letter".to_string());
}
if password.chars().any(|c| c.is_lowercase()) {
strength_score += 20;
} else {
errors.push("Password must contain at least one lowercase letter".to_string());
}
if password.chars().any(|c| c.is_numeric()) {
strength_score += 20;
} else {
errors.push("Password must contain at least one digit".to_string());
}
if password.chars().any(|c| c.is_ascii_punctuation()) {
strength_score += 20;
} else {
errors.push("Password must contain at least one special character".to_string());
}
// Additional strength checks
if password.len() >= 16 {
strength_score += 10;
}
if password.chars().filter(|c| c.is_ascii_punctuation()).count() >= 2 {
strength_score += 10;
}
let is_valid = errors.is_empty();
Self {
is_valid,
errors,
strength_score,
}
}
}
/// Session information
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
pub session_id: Uuid,
pub user_id: Uuid,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
pub created_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
pub last_activity: DateTime<Utc>,
pub is_active: bool,
}
impl SessionInfo {
/// Check if session is expired
pub fn is_expired(&self) -> bool {
Utc::now() > self.expires_at
}
/// Check if session is valid
pub fn is_valid(&self) -> bool {
self.is_active && !self.is_expired()
}
/// Update last activity
pub fn update_activity(&mut self) {
self.last_activity = Utc::now();
}
}
/// Permission check result
#[derive(Debug, Clone)]
pub struct PermissionCheck {
pub allowed: bool,
pub reason: Option<String>,
}
impl PermissionCheck {
/// Create allowed permission check
pub fn allowed() -> Self {
Self {
allowed: true,
reason: None,
}
}
/// Create denied permission check
pub fn denied(reason: String) -> Self {
Self {
allowed: false,
reason: Some(reason),
}
}
}
/// Role-based access control
pub struct RBAC {
roles: std::collections::HashMap<String, Vec<String>>,
}
impl RBAC {
/// Create new RBAC instance
pub fn new() -> Self {
Self {
roles: std::collections::HashMap::new(),
}
}
/// Add role with permissions
pub fn add_role(&mut self, role: &str, permissions: Vec<String>) {
self.roles.insert(role.to_string(), permissions);
}
/// Check if role has permission
pub fn has_permission(&self, role: &str, permission: &str) -> bool {
self.roles
.get(role)
.map(|permissions| permissions.contains(&permission.to_string()))
.unwrap_or(false)
}
/// Check if user has permission (check all roles)
pub fn user_has_permission(&self, user_roles: &[String], permission: &str) -> bool {
user_roles.iter().any(|role| self.has_permission(role, permission))
}
/// Get all permissions for a role
pub fn get_role_permissions(&self, role: &str) -> Vec<String> {
self.roles
.get(role)
.cloned()
.unwrap_or_default()
}
}
impl Default for RBAC {
fn default() -> Self {
let mut rbac = Self::new();
// Add default roles
rbac.add_role("admin", vec![
"user:read".to_string(),
"user:write".to_string(),
"user:delete".to_string(),
"conversation:read".to_string(),
"conversation:write".to_string(),
"conversation:delete".to_string(),
"track:read".to_string(),
"track:write".to_string(),
"track:delete".to_string(),
"system:admin".to_string(),
]);
rbac.add_role("user", vec![
"user:read".to_string(),
"conversation:read".to_string(),
"conversation:write".to_string(),
"track:read".to_string(),
"track:write".to_string(),
]);
rbac.add_role("guest", vec![
"track:read".to_string(),
]);
rbac
}
}
/// Generate secure random password
pub fn generate_secure_password(length: usize) -> VezaResult<String> {
use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*";
let mut rng = rand::thread_rng();
let password: String = (0..length)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect();
Ok(password)
}
/// Generate recovery code
pub fn generate_recovery_code() -> VezaResult<String> {
use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
let mut rng = rand::thread_rng();
let code: String = (0..8)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect();
Ok(code)
}
/// Generate TOTP secret
pub fn generate_totp_secret() -> VezaResult<String> {
use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
let mut rng = rand::thread_rng();
let secret: String = (0..32)
.map(|_| {
let idx = rng.gen_range(0..CHARSET.len());
CHARSET[idx] as char
})
.collect();
Ok(secret)
}
/// Validate TOTP code
pub fn validate_totp_code(secret: &str, code: &str, _window: i64) -> VezaResult<bool> {
use totp_rs::{TOTP, Algorithm, Secret};
// Use Secret::Encoded to handle base32 string directly
let secret_obj = Secret::Encoded(secret.to_string());
// Use TOTP::new with 5 arguments (basic validation)
let totp = TOTP::new(
Algorithm::SHA1,
6,
1,
30,
secret_obj.to_bytes()
.map_err(|e| VezaError::Auth(format!("Invalid TOTP secret: {}", e)))?,
).map_err(|e| VezaError::Auth(format!("Invalid TOTP secret: {}", e)))?;
let is_valid = totp.check_current(code)
.map_err(|e| VezaError::Auth(format!("TOTP validation error: {}", e)))?;
Ok(is_valid)
}
/// Generate QR code data for TOTP setup
pub fn generate_totp_qr_data(secret: &str, username: &str, issuer: &str) -> String {
format!(
"otpauth://totp/{}:{}?secret={}&issuer={}",
issuer, username, secret, issuer
)
}