kegani 0.1.1

A developer-friendly, ergonomic, production-ready Rust web framework
Documentation
//! Session cookie middleware for Kegani
//!
//! Reads the session cookie, loads the session from the store, and
//! makes it available as a request extension. Automatically creates
//! new sessions on first visit and persists changes on response.

use std::sync::Arc;
use actix_web::{
    body::EitherBody,
    dev::{ServiceRequest, ServiceResponse, Transform, Service},
    http::header::SET_COOKIE,
    HttpMessage,
};
use std::future::Ready;
use crate::session::{SessionStore, SessionConfig, Session};

/// Middleware factory for session cookie management
pub struct SessionCookie {
    store: Arc<dyn SessionStore>,
    config: SessionConfig,
}

impl SessionCookie {
    /// Create a new SessionCookie middleware
    pub fn new(store: Arc<dyn SessionStore>, config: SessionConfig) -> Self {
        Self { store, config }
    }
}

impl<S, B> Transform<S, ServiceRequest> for SessionCookie
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + Send + 'static,
    B: 'static,
{
    type Response = ServiceResponse<EitherBody<B>>;
    type Error = actix_web::Error;
    type Transform = SessionCookieMiddleware<S>;
    type InitError = ();
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        let middleware = SessionCookieMiddleware {
            service: Arc::new(std::sync::Mutex::new(Some(service))),
            store: self.store.clone(),
            config: self.config.clone(),
        };
        std::future::ready(Ok(middleware))
    }
}

pub struct SessionCookieMiddleware<S> {
    service: Arc<std::sync::Mutex<Option<S>>>,
    store: Arc<dyn SessionStore>,
    config: SessionConfig,
}

impl<S, B> Service<ServiceRequest> for SessionCookieMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + Send + 'static,
    B: 'static,
{
    type Response = ServiceResponse<EitherBody<B>>;
    type Error = actix_web::Error;
    type Future = std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>> + 'static>>;

    fn poll_ready(&self, _ctx: &mut std::task::Context<'_>) -> std::task::Poll<Result<(), Self::Error>> {
        // Delegate to inner service; we took ownership in call() so poll_ready is ok
        std::task::Poll::Ready(Ok(()))
    }

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let service = self.service.lock().unwrap().take().expect("service already taken");
        let store = self.store.clone();
        let config = self.config.clone();

        let cookie_name = config.name.clone();
        let session_id_opt = req.cookie(&cookie_name).map(|c| c.value().to_string());

        Box::pin(async move {
            let session = load_or_create_session(&store, &config, session_id_opt.as_deref()).await?;
            let session = Arc::new(tokio::sync::Mutex::new(session));
            req.extensions_mut().insert(session.clone());

            let response = service.call(req).await?.map_into_left_body();
            let current = session.lock().await.clone();
            if let Err(e) = store.save(&current).await {
                tracing::warn!("Failed to persist session: {}", e);
            }

            // Build Set-Cookie header as raw string (avoids Cookie lifetime issues)
            let same_site_str = match config.same_site {
                actix_web::cookie::SameSite::Strict => "Strict",
                actix_web::cookie::SameSite::Lax => "Lax",
                actix_web::cookie::SameSite::None => "None",
            };
            let cookie_str = format!(
                "{}={}; Path={}; HttpOnly; SameSite={}",
                config.name,
                current.id,
                config.path,
                same_site_str
            );
            let mut response = response;
            response.headers_mut().append(SET_COOKIE, cookie_str.parse().unwrap());
            Ok(response)
        })
    }
}

/// Load existing session or create a new one
async fn load_or_create_session(
    store: &Arc<dyn SessionStore>,
    config: &SessionConfig,
    cookie_value: Option<&str>,
) -> Result<Session, actix_web::Error> {
    match cookie_value {
        Some(id) => {
            match store.load(id).await {
                Ok(Some(s)) => Ok(s),
                Ok(None) => Ok(Session::new(config.ttl)),
                Err(e) => Err(actix_web::error::ErrorInternalServerError(format!(
                    "session error: {}", e
                ))),
            }
        }
        None => Ok(Session::new(config.ttl)),
    }
}

/// Extension trait for easy session access in handlers
pub trait SessionExt {
    fn session(&self) -> Option<Arc<tokio::sync::Mutex<Session>>>;
}

impl SessionExt for actix_web::HttpRequest {
    fn session(&self) -> Option<Arc<tokio::sync::Mutex<Session>>> {
        self.extensions().get::<Arc<tokio::sync::Mutex<Session>>>().cloned()
    }
}