tsa-auth-session 0.1.0

Session management for TSA
Documentation
use chrono::{Duration, Utc};
use std::sync::Arc;
use tsa_auth_core::{Result, Session, SessionRepository, TsaError};
use tsa_auth_token::OpaqueToken;
use uuid::Uuid;

#[derive(Debug, Clone)]
pub struct SessionConfig {
    pub session_expiry: Duration,
    pub token_length: usize,
}

impl Default for SessionConfig {
    fn default() -> Self {
        Self {
            session_expiry: Duration::days(30),
            token_length: 32,
        }
    }
}

pub struct SessionManager<R: SessionRepository> {
    repository: Arc<R>,
    config: SessionConfig,
}

impl<R: SessionRepository> SessionManager<R> {
    pub fn new(repository: Arc<R>, config: SessionConfig) -> Self {
        Self { repository, config }
    }

    pub async fn create_session(
        &self,
        user_id: Uuid,
        ip_address: Option<String>,
        user_agent: Option<String>,
    ) -> Result<(Session, String)> {
        let (token, token_hash) = OpaqueToken::generate_with_hash(self.config.token_length)?;

        let now = Utc::now();
        let session = Session {
            id: Uuid::new_v4(),
            user_id,
            token_hash,
            expires_at: now + self.config.session_expiry,
            created_at: now,
            ip_address,
            user_agent,
        };

        let created = self.repository.create(&session).await?;
        Ok((created, token))
    }

    pub async fn validate_session(&self, token: &str) -> Result<Session> {
        let token_hash = OpaqueToken::hash(token);
        let session = self
            .repository
            .find_by_token_hash(&token_hash)
            .await?
            .ok_or(TsaError::SessionNotFound)?;

        if session.expires_at < Utc::now() {
            self.repository.delete(session.id).await?;
            return Err(TsaError::SessionExpired);
        }

        Ok(session)
    }

    pub async fn invalidate_session(&self, session_id: Uuid) -> Result<()> {
        self.repository.delete(session_id).await
    }

    pub async fn invalidate_all_sessions(&self, user_id: Uuid) -> Result<()> {
        self.repository.delete_by_user_id(user_id).await
    }

    pub async fn get_user_sessions(&self, user_id: Uuid) -> Result<Vec<Session>> {
        self.repository.find_by_user_id(user_id).await
    }

    pub async fn cleanup_expired(&self) -> Result<u64> {
        self.repository.delete_expired().await
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_session_config_defaults() {
        let config = SessionConfig::default();
        assert_eq!(config.session_expiry, Duration::days(30));
        assert_eq!(config.token_length, 32);
    }

    #[test]
    fn test_session_config_custom() {
        let config = SessionConfig {
            session_expiry: Duration::hours(1),
            token_length: 64,
        };
        assert_eq!(config.session_expiry, Duration::hours(1));
        assert_eq!(config.token_length, 64);
    }
}