seshcookie 0.1.0

Stateless, encrypted, type-safe session cookies for Rust web applications.
//! Minimal login / me / logout flow against an in-memory user "store"
//! (the example just trusts the request — there is no real password check).
//!
//! Run with:
//!
//! ```sh
//! cargo run --example axum_login
//! ```
//!
//! The example listens on `127.0.0.1:3000`. A quick smoke test:
//!
//! ```sh
//! curl -c /tmp/jar -X POST http://127.0.0.1:3000/login \
//!     -H 'content-type: application/json' -d '{"name":"alice"}'
//! curl -b /tmp/jar http://127.0.0.1:3000/me
//! curl -b /tmp/jar -X POST http://127.0.0.1:3000/logout
//! ```
//!
//! The example demonstrates the full session lifecycle (typed payload,
//! `Set-Cookie` round-trip, optional extraction for unauthenticated paths,
//! explicit clear on logout) on the smallest realistic surface.

use axum::{
    Router,
    extract::Json,
    http::StatusCode,
    response::IntoResponse,
    routing::{get, post},
};
use serde::{Deserialize, Serialize};
use seshcookie::{SameSite, Session, SessionConfig, SessionKeys, SessionLayer};

#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
struct User {
    id: u64,
    name: String,
}

#[derive(Deserialize)]
struct LoginRequest {
    name: String,
}

async fn login(session: Session<User>, Json(req): Json<LoginRequest>) -> impl IntoResponse {
    // Real applications would validate credentials before issuing a session.
    // This example is intentionally credentials-free so the focus stays on
    // the seshcookie-rs surface.
    let user = User {
        id: 1,
        name: req.name,
    };
    session.insert(user).await;
    StatusCode::NO_CONTENT
}

async fn me(session: Option<Session<User>>) -> impl IntoResponse {
    match session {
        Some(s) => match s.get().await {
            Some(u) => (StatusCode::OK, Json(u)).into_response(),
            None => StatusCode::UNAUTHORIZED.into_response(),
        },
        None => StatusCode::UNAUTHORIZED.into_response(),
    }
}

async fn logout(session: Session<User>) -> impl IntoResponse {
    session.clear().await;
    StatusCode::NO_CONTENT
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Real deployments load the secret from the environment, a secrets
    // manager, or a KMS. The hard-coded value here is fine for a localhost
    // demo and would be a serious bug in any other context.
    let secret = b"0123456789abcdef0123456789abcdef";
    let keys = SessionKeys::new(secret)?;
    let config = SessionConfig::default()
        .secure(false)
        .same_site(SameSite::Lax);
    let layer = SessionLayer::<User>::new(keys, config)?;

    let app = Router::new()
        .route("/login", post(login))
        .route("/me", get(me))
        .route("/logout", post(logout))
        .layer(layer);

    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
    println!("listening on http://127.0.0.1:3000");
    axum::serve(listener, app).await?;
    Ok(())
}