threatflux-core 0.2.0

Core types and utilities for ThreatFlux security platform
Documentation
use std::env;

use crate::error::AuthError;

#[derive(Debug, Clone)]
pub struct PostgresStoreConfig {
    pub url: String,
    pub max_connections: Option<u32>,
}

impl PostgresStoreConfig {
    #[must_use]
    pub fn new(url: impl Into<String>) -> Self {
        Self {
            url: url.into(),
            max_connections: None,
        }
    }

    #[must_use]
    pub const fn with_max_connections(mut self, max: u32) -> Self {
        self.max_connections = Some(max);
        self
    }
}

#[derive(Debug, Clone)]
pub struct AuthConfig {
    pub secret: String,
    pub database: Option<PostgresStoreConfig>,
}

impl AuthConfig {
    pub fn from_env() -> Result<Self, AuthError> {
        let secret = env::var("AUTH_SECRET")
            .or_else(|_| env::var("JWT_SECRET"))
            .map_err(|_| AuthError::BadRequest("AUTH_SECRET is required".to_string()))?;

        let database_url = env::var("DATABASE_URL").ok();
        let max_connections = env::var("DATABASE_MAX_CONNECTIONS")
            .ok()
            .and_then(|value| value.parse::<u32>().ok());

        let database = database_url.map(|url| PostgresStoreConfig {
            url,
            max_connections,
        });

        Ok(Self { secret, database })
    }
}

// ============================================================================
// MFA Configuration
// ============================================================================

/// Configuration for Multi-Factor Authentication
#[derive(Debug, Clone)]
pub struct MfaConfig {
    /// Issuer name shown in authenticator apps (e.g., `MyApp`)
    pub issuer: String,
    /// Number of backup codes to generate (default: 8)
    pub backup_code_count: usize,
    /// Partial session expiry in minutes (default: 5)
    pub partial_session_expiry_minutes: i64,
}

impl Default for MfaConfig {
    fn default() -> Self {
        Self {
            issuer: "ThreatFlux".to_string(),
            backup_code_count: 8,
            partial_session_expiry_minutes: 5,
        }
    }
}

impl MfaConfig {
    pub fn from_env() -> Self {
        Self {
            issuer: env::var("MFA_ISSUER").unwrap_or_else(|_| "ThreatFlux".to_string()),
            backup_code_count: env::var("MFA_BACKUP_CODE_COUNT")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(8),
            partial_session_expiry_minutes: env::var("MFA_PARTIAL_SESSION_EXPIRY_MINUTES")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(5),
        }
    }
}

// ============================================================================
// Token Configuration (Magic Links, Password Reset)
// ============================================================================

/// Configuration for authentication tokens
#[derive(Debug, Clone)]
pub struct TokenConfig {
    /// Magic link expiry in minutes (default: 15)
    pub magic_link_expiry_minutes: i64,
    /// Password reset token expiry in minutes (default: 60)
    pub password_reset_expiry_minutes: i64,
    /// Rate limit - max tokens per hour per user (default: 3)
    pub rate_limit_per_hour: i32,
}

impl Default for TokenConfig {
    fn default() -> Self {
        Self {
            magic_link_expiry_minutes: 15,
            password_reset_expiry_minutes: 60,
            rate_limit_per_hour: 3,
        }
    }
}

impl TokenConfig {
    pub fn from_env() -> Self {
        Self {
            magic_link_expiry_minutes: env::var("MAGIC_LINK_EXPIRY_MINUTES")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(15),
            password_reset_expiry_minutes: env::var("PASSWORD_RESET_EXPIRY_MINUTES")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(60),
            rate_limit_per_hour: env::var("TOKEN_RATE_LIMIT_PER_HOUR")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(3),
        }
    }
}

// ============================================================================
// Device Configuration
// ============================================================================

/// Configuration for device tracking
#[derive(Debug, Clone)]
pub struct DeviceConfig {
    /// How long a device stays trusted in days (default: 90)
    pub trust_duration_days: i64,
}

impl Default for DeviceConfig {
    fn default() -> Self {
        Self {
            trust_duration_days: 90,
        }
    }
}

impl DeviceConfig {
    pub fn from_env() -> Self {
        Self {
            trust_duration_days: env::var("DEVICE_TRUST_DURATION_DAYS")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(90),
        }
    }
}

// ============================================================================
// WebAuthn Configuration
// ============================================================================

/// Configuration for WebAuthn/Passkeys
#[derive(Debug, Clone)]
pub struct WebAuthnConfig {
    /// Relying Party ID (usually your domain, e.g., "example.com")
    pub rp_id: String,
    /// Relying Party name shown to users
    pub rp_name: String,
    /// Relying Party origin URL (e.g., `https://example.com`)
    pub rp_origin: String,
    /// Challenge TTL in minutes (default: 5)
    pub challenge_ttl_minutes: i64,
}

impl Default for WebAuthnConfig {
    fn default() -> Self {
        Self {
            rp_id: "localhost".to_string(),
            rp_name: "ThreatFlux".to_string(),
            rp_origin: "http://localhost:8080".to_string(),
            challenge_ttl_minutes: 5,
        }
    }
}

impl WebAuthnConfig {
    pub fn from_env() -> Self {
        Self {
            rp_id: env::var("WEBAUTHN_RP_ID").unwrap_or_else(|_| "localhost".to_string()),
            rp_name: env::var("WEBAUTHN_RP_NAME").unwrap_or_else(|_| "ThreatFlux".to_string()),
            rp_origin: env::var("WEBAUTHN_RP_ORIGIN")
                .unwrap_or_else(|_| "http://localhost:8080".to_string()),
            challenge_ttl_minutes: env::var("WEBAUTHN_CHALLENGE_TTL_MINUTES")
                .ok()
                .and_then(|v| v.parse().ok())
                .unwrap_or(5),
        }
    }
}

// ============================================================================
// Encryption Configuration
// ============================================================================

/// Configuration for encryption services
#[derive(Debug, Clone)]
pub struct EncryptionConfig {
    /// Hex-encoded 32-byte encryption key (64 characters)
    pub key: String,
}

impl EncryptionConfig {
    pub fn from_env() -> Result<Self, AuthError> {
        let key = env::var("ENCRYPTION_KEY")
            .or_else(|_| env::var("PLAID_ENCRYPTION_KEY"))
            .map_err(|_| AuthError::BadRequest("ENCRYPTION_KEY is required".to_string()))?;

        if key.len() != 64 {
            return Err(AuthError::BadRequest(
                "ENCRYPTION_KEY must be 64 hex characters (32 bytes)".to_string(),
            ));
        }

        Ok(Self { key })
    }
}