use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; use axum_extra::extract::cookie::{Cookie, SameSite}; use rand::Rng; use rand::distributions::Alphanumeric; use tokio::sync::RwLock; pub struct WebSession { pub slug: String, pub role: String, pub expires_at: Instant, /// CSRF token — bound to the session for its full lifetime. Minted /// alongside the session cookie via [`generate_token`] and validated /// by the `CsrfForm` extractor on every state-changing POST under /// `/web/`. Not rotated per request (would break multi-tab use). pub csrf_token: String, } pub type SessionStore = Arc>>; pub fn new_store() -> SessionStore { Arc::new(RwLock::new(HashMap::new())) } /// Cookie name shared by login and magic-link flows. pub const SESSION_COOKIE: &str = "session"; /// 43 alphanumeric chars ≈ 256 bits of entropy — enough for a session /// or short-lived magic-link token. pub const TOKEN_LEN: usize = 43; /// CSPRNG-backed token. Used for both long-lived session cookies and /// short-lived magic-link tokens; both need the same entropy. pub fn generate_token() -> String { rand::thread_rng() .sample_iter(&Alphanumeric) .take(TOKEN_LEN) .map(char::from) .collect() } /// Builds a `HttpOnly + SameSite=Strict + Path=/` cookie for the session /// token. `secure` is wired from `Config::cookie_secure` so local HTTP dev /// can opt out — browsers refuse `Secure` cookies on non-HTTPS origins. pub fn build_session_cookie(value: String, max_age: Duration, secure: bool) -> Cookie<'static> { Cookie::build((SESSION_COOKIE, value)) .http_only(true) .secure(secure) .same_site(SameSite::Strict) .path("/") .max_age(time::Duration::seconds(max_age.as_secs() as i64)) .build() }